Skip to content
This repository has been archived by the owner on Jun 8, 2022. It is now read-only.

implement trait.ApplyTo through AppConfig validating webhook #310

Merged
merged 1 commit into from
Nov 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/google/go-cmp v0.4.0
github.com/gophercloud/gophercloud v0.6.0 // indirect
github.com/json-iterator/go v1.1.10
github.com/kr/pretty v0.2.0 // indirect
github.com/onsi/ginkgo v1.12.1
github.com/onsi/gomega v1.10.1
Expand Down
147 changes: 61 additions & 86 deletions pkg/webhook/v1alpha2/applicationconfiguration/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,113 +194,88 @@ var _ = Describe("ApplicationConfiguration Admission controller Test", func() {
var handler admission.Handler = &ValidatingHandler{Mapper: mapper}
decoderInjector := handler.(admission.DecoderInjector)
decoderInjector.InjectDecoder(decoder)
By("Creating valid trait")
validTrait := unstructured.Unstructured{}
validTrait.SetAPIVersion("validAPI")
validTrait.SetKind("validKind")
By("Creating invalid trait with type")
traitWithType := validTrait.DeepCopy()
typeContent := make(map[string]interface{})
typeContent[TraitTypeField] = "should not be here"
traitWithType.SetUnstructuredContent(typeContent)
By("Creating invalid trait without kind")
noKindTrait := validTrait.DeepCopy()
noKindTrait.SetKind("")
var traitTypeName = "test-trait"
traitDef := v1alpha2.TraitDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: traitTypeName,
Labels: label,

testWorkload := unstructured.Unstructured{}
testWorkload.SetAPIVersion("example.com/v1")
testWorkload.SetKind("TestWorkload")

testComponent := v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "example.com/v1",
Kind: "TestComponent",
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Raw: util.JSONMarshal(testWorkload.Object),
},
},
Status: v1alpha2.ComponentStatus{
LatestRevision: &v1alpha2.Revision{
Name: "example-comp-v1",
},
},
}

testWorkloadDef := v1alpha2.WorkloadDefinition{
TypeMeta: metav1.TypeMeta{
APIVersion: "example.com/v1",
Kind: "TestWorkload",
},
Spec: v1alpha2.TraitDefinitionSpec{
Reference: v1alpha2.DefinitionReference{
Name: "foos.example.com",
}
testTrait := unstructured.Unstructured{}
testTrait.SetAPIVersion("example.com/v1")
testTrait.SetKind("TestTrait")
appConfig.Spec.Components[0] = v1alpha2.ApplicationConfigurationComponent{
ComponentName: "example-comp",
Traits: []v1alpha2.ComponentTrait{
{
Trait: runtime.RawExtension{Raw: util.JSONMarshal(testTrait.Object)},
},
},
}
testTraitDef := v1alpha2.TraitDefinition{
TypeMeta: metav1.TypeMeta{
APIVersion: "example.com/v1",
Kind: "TestTrait",
},
}

clientInstance := &test.MockClient{
MockGet: func(ctx context.Context, key types.NamespacedName, obj runtime.Object) error {
switch o := obj.(type) {
case *v1alpha2.Component:
*o = testComponent
case *v1alpha2.WorkloadDefinition:
*o = testWorkloadDef
case *v1alpha2.TraitDefinition:
*o = traitDef
case *crdv1.CustomResourceDefinition:
Expect(key.Name).Should(Equal(traitDef.Spec.Reference.Name))
*o = crd
*o = testTraitDef
}
return nil
},
}
tests := map[string]struct {
trait interface{}
client client.Client
operation admissionv1beta1.Operation
pass bool
reason string
}{
"valid create case": {
trait: validTrait.DeepCopyObject(),
operation: admissionv1beta1.Create,
pass: true,
reason: "",
client: clientInstance,
},
"valid update case": {
trait: validTrait.DeepCopyObject(),
operation: admissionv1beta1.Update,
pass: true,
reason: "",
client: clientInstance,
},
"malformat appConfig": {
trait: "bad format",
operation: admissionv1beta1.Create,
pass: false,
reason: "the trait is malformed",
client: clientInstance,
},
"trait still has type": {
trait: traitWithType.DeepCopyObject(),
operation: admissionv1beta1.Create,
pass: false,
reason: "the trait contains 'name' info",
client: clientInstance,
},
"no kind trait appConfig": {
trait: noKindTrait.DeepCopyObject(),
operation: admissionv1beta1.Update,
pass: false,
reason: "the trait data missing GVK",
client: clientInstance,

req := admission.Request{
AdmissionRequest: admissionv1beta1.AdmissionRequest{
Operation: admissionv1beta1.Create,
Resource: reqResource,
Object: runtime.RawExtension{Raw: util.JSONMarshal(appConfig)},
},
}
for testCase, test := range tests {
By(fmt.Sprintf("start test : %s", testCase))
appConfig.Spec.Components[0].Traits[0].Trait = runtime.RawExtension{Raw: util.JSONMarshal(test.trait)}
req := admission.Request{
AdmissionRequest: admissionv1beta1.AdmissionRequest{
Operation: test.operation,
Resource: reqResource,
Object: runtime.RawExtension{Raw: util.JSONMarshal(appConfig)},
},
}
injc := handler.(inject.Client)
injc.InjectClient(test.client)
resp := handler.Handle(context.TODO(), req)
Expect(resp.Allowed).Should(Equal(test.pass))
if !test.pass {
Expect(string(resp.Result.Reason)).Should(ContainSubstring(test.reason))
}
}
injc := handler.(inject.Client)
injc.InjectClient(clientInstance)
resp := handler.Handle(context.TODO(), req)
By(string(resp.Result.Reason))
Expect(resp.Allowed).Should(BeTrue())

By("Test bad admission request format")
req := admission.Request{
req = admission.Request{
AdmissionRequest: admissionv1beta1.AdmissionRequest{
Operation: admissionv1beta1.Create,
Resource: reqResource,
Object: runtime.RawExtension{Raw: []byte("bad request")},
},
}
resp := handler.Handle(context.TODO(), req)
resp = handler.Handle(context.TODO(), req)
Expect(resp.Allowed).Should(BeFalse())
})

})
109 changes: 88 additions & 21 deletions pkg/webhook/v1alpha2/applicationconfiguration/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"strings"

"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand All @@ -16,32 +15,100 @@ import (
)

const (
errUnmarshalTrait = "cannot unmarshal trait"
errFmtGetTraitDefinition = "cannot find trait definition %q %q %q"
errFmtGetComponent = "cannot get component %q"
errFmtGetTraitDefinition = "cannot get trait definition in component %q"
errFmtUnmarshalWorkload = "cannot unmarshal workload of component %q"
errFmtUnmarshalTrait = "cannot unmarshal trait of component %q"
errFmtGetWorkloadDefinition = "cannot get workload definition of component %q"
)

// checkComponentVersionEnabled check whethter a component is versioning mechanism enabled
func checkComponentVersionEnabled(ctx context.Context, client client.Reader, dm discoverymapper.DiscoveryMapper,
acc *v1alpha2.ApplicationConfigurationComponent) (bool, error) {
if acc.RevisionName != "" {
return true, nil
}
for _, ct := range acc.Traits {
ut := &unstructured.Unstructured{}
if err := json.Unmarshal(ct.Trait.Raw, ut); err != nil {
return false, errors.Wrap(err, errUnmarshalTrait)
// ValidatingAppConfig is used for validating ApplicationConfiguration
type ValidatingAppConfig struct {
appConfig v1alpha2.ApplicationConfiguration
validatingComps []ValidatingComponent
}

// ValidatingComponent is used for validatiing ApplicationConfigurationComponent
type ValidatingComponent struct {
appConfigComponent v1alpha2.ApplicationConfigurationComponent

// below data is convenient for validation
compName string
component v1alpha2.Component
workloadDefinition v1alpha2.WorkloadDefinition
workloadContent unstructured.Unstructured
validatingTraits []ValidatingTrait
}

// ValidatingTrait is used for validating Trait
type ValidatingTrait struct {
componentTrait v1alpha2.ComponentTrait

// below data is convenient for validation
traitDefinition v1alpha2.TraitDefinition
traitContent unstructured.Unstructured
}

// PrepareForValidation prepares data for validations to avoiding repetitive GET/unmarshal operations
func (v *ValidatingAppConfig) PrepareForValidation(ctx context.Context, c client.Reader, dm discoverymapper.DiscoveryMapper, ac *v1alpha2.ApplicationConfiguration) error {
v.appConfig = *ac
v.validatingComps = make([]ValidatingComponent, 0, len(ac.Spec.Components))
for _, acc := range ac.Spec.Components {
tmp := ValidatingComponent{}
tmp.appConfigComponent = acc

if acc.ComponentName != "" {
tmp.compName = acc.ComponentName
} else {
tmp.compName = acc.RevisionName
}
td, err := util.FetchTraitDefinition(ctx, client, dm, ut)
if err != nil && !apierrors.IsNotFound(err) {
return false, errors.Wrapf(err, errFmtGetTraitDefinition, ut.GetAPIVersion(), ut.GetKind(), ut.GetName())
comp, _, err := util.GetComponent(ctx, c, acc, ac.Namespace)
if err != nil {
return errors.Wrapf(err, errFmtGetComponent, tmp.compName)
}
if td.Spec.RevisionEnabled {
// if any traitDefinition's RevisionEnabled is true
// then the component is versioning enabled
return true, nil
tmp.component = *comp

// get worload content from raw
var wlContentObject map[string]interface{}
if err := json.Unmarshal(comp.Spec.Workload.Raw, &wlContentObject); err != nil {
return errors.Wrapf(err, errFmtUnmarshalWorkload, tmp.compName)
}
wl := unstructured.Unstructured{
Object: wlContentObject,
}
tmp.workloadContent = wl

// get workload definition
wlDef, err := util.FetchWorkloadDefinition(ctx, c, dm, &wl)
if err != nil {
return errors.Wrapf(err, errFmtGetWorkloadDefinition, tmp.compName)
}
tmp.workloadDefinition = *wlDef

tmp.validatingTraits = make([]ValidatingTrait, 0, len(acc.Traits))
for _, t := range acc.Traits {
tmpT := ValidatingTrait{}
tmpT.componentTrait = t
// get trait content from raw
var tContentObject map[string]interface{}
if err := json.Unmarshal(t.Trait.Raw, &tContentObject); err != nil {
return errors.Wrapf(err, errFmtUnmarshalTrait, tmp.compName)
}
tContent := unstructured.Unstructured{
Object: tContentObject,
}
// get trait definition
tDef, err := util.FetchTraitDefinition(ctx, c, dm, &tContent)
if err != nil {
return errors.Wrapf(err, errFmtGetTraitDefinition, tmp.compName)
}
tmpT.traitContent = tContent
tmpT.traitDefinition = *tDef
tmp.validatingTraits = append(tmp.validatingTraits, tmpT)
}
v.validatingComps = append(v.validatingComps, tmp)
}
return false, nil
return nil
}

// checkParams will check whether exist parameter assigning value to workload name
Expand Down
Loading