diff --git a/pkg/api/v1alpha1/clusterctl_types.go b/pkg/api/v1alpha1/clusterctl_types.go index a9a50e6d5..ca5b21300 100644 --- a/pkg/api/v1alpha1/clusterctl_types.go +++ b/pkg/api/v1alpha1/clusterctl_types.go @@ -16,15 +16,9 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" ) -var ( - // GroupVersionKind is group version used to register these objects - GroupVersionKind = schema.GroupVersionKind{Group: "airshipit.org", Version: "v1alpha1", Kind: "Clusterctl"} -) - // +kubebuilder:object:root=true // Clusterctl provides information about clusterctl components diff --git a/pkg/clusterctl/cmd/command.go b/pkg/clusterctl/cmd/command.go index 33cad7484..540bcbe49 100644 --- a/pkg/clusterctl/cmd/command.go +++ b/pkg/clusterctl/cmd/command.go @@ -15,8 +15,6 @@ package cmd import ( - "sigs.k8s.io/yaml" - airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/clusterctl/client" "opendev.org/airship/airshipctl/pkg/config" @@ -71,21 +69,22 @@ func (c *Command) Init() error { } func clusterctlOptions(bundle document.Bundle) (*airshipv1.Clusterctl, error) { - doc, err := bundle.SelectOne(document.NewClusterctlSelector()) + cctl := &airshipv1.Clusterctl{} + selector, err := document.NewSelector().ByObject(cctl, airshipv1.Scheme) if err != nil { return nil, err } - options := &airshipv1.Clusterctl{} - b, err := doc.AsYAML() + + doc, err := bundle.SelectOne(selector) if err != nil { return nil, err } - // TODO (kkalynovskyi) instead of this, use kubernetes serializer - err = yaml.Unmarshal(b, options) - if err != nil { + + if err := doc.ToAPIObject(cctl, airshipv1.Scheme); err != nil { return nil, err } - return options, nil + + return cctl, nil } func getBundle(conf *config.Config) (document.Bundle, error) { diff --git a/pkg/document/document.go b/pkg/document/document.go index bfa899f4f..5da9bba4f 100644 --- a/pkg/document/document.go +++ b/pkg/document/document.go @@ -15,6 +15,9 @@ package document import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct" "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/yaml" @@ -47,6 +50,7 @@ type Document interface { Label(map[string]string) MarshalJSON() ([]byte, error) ToObject(interface{}) error + ToAPIObject(runtime.Object, *runtime.Scheme) error } // Factory implements Document @@ -188,6 +192,23 @@ func (d *Factory) ToObject(obj interface{}) error { return yaml.Unmarshal(docYAML, obj) } +// ToAPIObject de-serializes a document into a runtime.Object +func (d *Factory) ToAPIObject(obj runtime.Object, scheme *runtime.Scheme) error { + y, err := d.AsYAML() + if err != nil { + return err + } + + yamlSerializer := json.NewSerializerWithOptions( + json.DefaultMetaFactory, + scheme, + scheme, + json.SerializerOptions{Yaml: true, Pretty: false, Strict: false}) + + _, _, err = yamlSerializer.Decode(y, nil, obj) + return err +} + // NewDocument is a convenience method to construct a new Document. Although // an error is unlikely at this time, this provides some future proofing for // when we want more strict airship specific validation of documents getting diff --git a/pkg/document/document_test.go b/pkg/document/document_test.go index f18967ef3..848512c95 100644 --- a/pkg/document/document_test.go +++ b/pkg/document/document_test.go @@ -21,6 +21,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + airapiv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/testutil" ) @@ -102,6 +105,33 @@ func TestDocument(t *testing.T) { assert.Equal(expectedObj, actualObj) }) + t.Run("ToAPIObject", func(t *testing.T) { + expectedObj := &airapiv1.Clusterctl{ + TypeMeta: metav1.TypeMeta{ + Kind: "Clusterctl", + APIVersion: "airshipit.org/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterctl-v1", + }, + Providers: []*airapiv1.Provider{ + { + Name: "aws", + Type: "InfrastructureProvider", + URL: "/manifests/capi/infra/aws/v0.3.0", + }, + }, + } + sel, err := document.NewSelector().ByObject(expectedObj, airapiv1.Scheme) + require.NoError(err) + doc, err := bundle.SelectOne(sel) + require.NoError(err) + actualObj := &airapiv1.Clusterctl{} + err = doc.ToAPIObject(actualObj, airapiv1.Scheme) + assert.NoError(err) + assert.Equal(expectedObj, actualObj) + }) + t.Run("GetString", func(t *testing.T) { doc, err := bundle.GetByName("some-random-deployment-we-will-filter") require.NoError(err, "Unexpected error trying to GetByName") diff --git a/pkg/document/errors.go b/pkg/document/errors.go index b7c7e2ff6..70cb0c789 100644 --- a/pkg/document/errors.go +++ b/pkg/document/errors.go @@ -16,6 +16,8 @@ package document import ( "fmt" + + "k8s.io/apimachinery/pkg/runtime" ) // ErrDocNotFound returned if desired document not found by selector @@ -41,6 +43,12 @@ type ErrDocumentMalformed struct { Message string } +// ErrRuntimeObjectKind returned if runtime object contains either none or +// more than one Kinds defined in schema +type ErrRuntimeObjectKind struct { + Obj runtime.Object +} + func (e ErrDocNotFound) Error() string { return fmt.Sprintf("document filtered by selector %v found no documents", e.Selector) } @@ -56,3 +64,7 @@ func (e ErrDocumentDataKeyNotFound) Error() string { func (e ErrDocumentMalformed) Error() string { return fmt.Sprintf("document %q is malformed: %q", e.DocName, e.Message) } + +func (e ErrRuntimeObjectKind) Error() string { + return fmt.Sprintf("object %#v has either none or multiple kinds in scheme (expected one)", e.Obj) +} diff --git a/pkg/document/selectors.go b/pkg/document/selectors.go index 0d1132969..e46d10773 100644 --- a/pkg/document/selectors.go +++ b/pkg/document/selectors.go @@ -18,10 +18,11 @@ import ( "fmt" "strings" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/types" - - airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" ) // Selector provides abstraction layer in front of kustomize selector @@ -81,6 +82,28 @@ func (s Selector) ByAnnotation(annotationSelector string) Selector { return s } +// ByObject select by runtime object defined in API schema +func (s Selector) ByObject(obj runtime.Object, scheme *runtime.Scheme) (Selector, error) { + gvks, _, err := scheme.ObjectKinds(obj) + if err != nil { + return Selector{}, err + } + + if len(gvks) != 1 { + return Selector{}, ErrRuntimeObjectKind{Obj: obj} + } + result := NewSelector().ByGvk(gvks[0].Group, gvks[0].Version, gvks[0].Kind) + + accessor, err := meta.Accessor(obj) + if err != nil { + return Selector{}, err + } + if name := accessor.GetName(); name != "" { + result = result.ByName(name) + } + return result, nil +} + // String is a convenience function which dumps all relevant information about a Selector in the following format: // [Key1=Value1, Key2=Value2, ...] func (s Selector) String() string { @@ -166,12 +189,3 @@ func NewClusterctlMetadataSelector() Selector { ClusterctlMetadataVersion, ClusterctlMetadataKind) } - -// NewClusterctlSelector returns a selector to get document that controls how clusterctl -// components will be applied -func NewClusterctlSelector() Selector { - return NewSelector().ByGvk( - airshipv1.GroupVersionKind.Group, - airshipv1.GroupVersionKind.Version, - airshipv1.GroupVersionKind.Kind) -} diff --git a/pkg/document/selectors_test.go b/pkg/document/selectors_test.go index 060227773..f87a91ed1 100644 --- a/pkg/document/selectors_test.go +++ b/pkg/document/selectors_test.go @@ -19,7 +19,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + k8sv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/kustomize/api/resid" + "sigs.k8s.io/kustomize/api/types" + airapiv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1" "opendev.org/airship/airshipctl/pkg/document" "opendev.org/airship/airshipctl/testutil" ) @@ -54,12 +60,6 @@ func TestSelectorsPositive(t *testing.T) { require.NoError(t, err) assert.Len(t, doc, 1) }) - - t.Run("TestNewClusterctlSelector", func(t *testing.T) { - docs, err := bundle.Select(document.NewClusterctlSelector()) - require.NoError(t, err) - assert.Len(t, docs, 1) - }) } func TestSelectorsNegative(t *testing.T) { @@ -142,3 +142,65 @@ func TestSelectorString(t *testing.T) { }) } } + +func TestSelectorToObject(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + expectedSel document.Selector + expectedErr string + }{ + { + name: "Selector with GVK", + obj: &airapiv1.Clusterctl{}, + expectedSel: document.Selector{ + Selector: types.Selector{ + Gvk: resid.Gvk{ + Group: "airshipit.org", + Version: "v1alpha1", + Kind: "Clusterctl", + }, + }, + }, + expectedErr: "", + }, + { + name: "Unregistered object", + obj: &k8sv1.Pod{}, + expectedSel: document.Selector{}, + expectedErr: "no kind is registered for the type v1.Pod in scheme", + }, + { + name: "Selector with GVK and Name", + obj: &airapiv1.Clusterctl{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterctl-v1", + }, + }, + expectedSel: document.Selector{ + Selector: types.Selector{ + Gvk: resid.Gvk{ + Group: "airshipit.org", + Version: "v1alpha1", + Kind: "Clusterctl", + }, + Name: "clusterctl-v1", + }, + }, + expectedErr: "", + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + actualSel, err := document.NewSelector(). + ByObject(tt.obj, airapiv1.Scheme) + if test.expectedErr != "" { + assert.Contains(t, err.Error(), tt.expectedErr) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedSel, actualSel) + } + }) + } +} diff --git a/pkg/document/testdata/common/clusterctl.yaml b/pkg/document/testdata/common/clusterctl.yaml new file mode 100644 index 000000000..51a7afe7e --- /dev/null +++ b/pkg/document/testdata/common/clusterctl.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: airshipit.org/v1alpha1 +kind: Clusterctl +metadata: + name: clusterctl-v1 +providers: +- name: "aws" + type: "InfrastructureProvider" + url: "/manifests/capi/infra/aws/v0.3.0" \ No newline at end of file diff --git a/pkg/document/testdata/common/kustomization.yaml b/pkg/document/testdata/common/kustomization.yaml index 185967316..9bac53546 100644 --- a/pkg/document/testdata/common/kustomization.yaml +++ b/pkg/document/testdata/common/kustomization.yaml @@ -3,4 +3,5 @@ resources: - tiller.yaml - argo.yaml - initially_ignored.yaml - - custom_resource.yaml \ No newline at end of file + - custom_resource.yaml + - clusterctl.yaml \ No newline at end of file