Implement document filtering using api objects

Change-Id: I9ac70ed85c949224d091e9e3afead70084b38170
Co-Authored-By: Vladimir Kozhukalov <kozhukalov@gmail.com>
This commit is contained in:
Dmitry Ukov 2020-06-11 09:18:04 +04:00
parent cad8177fee
commit c05286d3f6
9 changed files with 175 additions and 33 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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")

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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"

View File

@ -3,4 +3,5 @@ resources:
- tiller.yaml
- argo.yaml
- initially_ignored.yaml
- custom_resource.yaml
- custom_resource.yaml
- clusterctl.yaml