diff --git a/README.md b/README.md index 7c1236033..16ed96b0a 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,16 @@ go build -v ./... go test -v -failfast -count=1 ./... ``` +### Storage operations + +During storage operations there are several opportunities to either reject the request or modify the stored object before it is written. + +Each type of operation (Create/Update/Delete) has its own set of functions that will run in the lifecycle of the request. + +These functions are declared in `pkg/registry/softwarecomposition//strategy.go` + +Read more about each function and its use [here](https://github.com/kubernetes-sigs/apiserver-builder-alpha/blob/master/docs/concepts/api_building_overview.md#storage-operations) + ### Authentication plugins The normal build supports only a very spare selection of diff --git a/pkg/registry/softwarecomposition/applicationprofile/strategy.go b/pkg/registry/softwarecomposition/applicationprofile/strategy.go index 89ac508c9..c359bed6a 100644 --- a/pkg/registry/softwarecomposition/applicationprofile/strategy.go +++ b/pkg/registry/softwarecomposition/applicationprofile/strategy.go @@ -12,7 +12,9 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" + "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" "github.com/kubescape/storage/pkg/apis/softwarecomposition" + "github.com/kubescape/storage/pkg/utils" ) // NewStrategy creates and returns a applicationProfileStrategy instance @@ -57,10 +59,36 @@ func (applicationProfileStrategy) PrepareForCreate(ctx context.Context, obj runt } func (applicationProfileStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newAP := obj.(*softwarecomposition.ApplicationProfile) + oldAP := old.(*softwarecomposition.ApplicationProfile) + + // completion status cannot be transitioned from 'complete' -> 'partial' + // in such case, we reject status updates + if oldAP.Annotations[helpers.CompletionMetadataKey] == helpers.Complete && newAP.Annotations[helpers.CompletionMetadataKey] == helpers.Partial { + newAP.Annotations[helpers.CompletionMetadataKey] = helpers.Complete + + if v, ok := oldAP.Annotations[helpers.StatusMetadataKey]; ok { + newAP.Annotations[helpers.StatusMetadataKey] = v + } else { + delete(newAP.Annotations, helpers.StatusMetadataKey) + } + } } func (applicationProfileStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return field.ErrorList{} + ap := obj.(*softwarecomposition.ApplicationProfile) + + allErrors := field.ErrorList{} + + if err := utils.ValidateCompletionAnnotation(ap.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + if err := utils.ValidateStatusAnnotation(ap.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + return allErrors } // WarningsOnCreate returns warnings for the creation of the given object. @@ -80,7 +108,19 @@ func (applicationProfileStrategy) Canonicalize(obj runtime.Object) { } func (applicationProfileStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return field.ErrorList{} + ap := obj.(*softwarecomposition.ApplicationProfile) + + allErrors := field.ErrorList{} + + if err := utils.ValidateCompletionAnnotation(ap.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + if err := utils.ValidateStatusAnnotation(ap.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + return allErrors } // WarningsOnUpdate returns warnings for the given update. diff --git a/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go b/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go new file mode 100644 index 000000000..77bb45ac4 --- /dev/null +++ b/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go @@ -0,0 +1,92 @@ +package applicationprofile + +import ( + "context" + "reflect" + "testing" + + "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/kubescape/storage/pkg/apis/softwarecomposition" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPrepareForUpdate(t *testing.T) { + tests := []struct { + name string + oldAnnotations map[string]string + newAnnotations map[string]string + expected map[string]string + }{ + { + name: "transition from complete (with status) to partial - rejected", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "initializing", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "ready", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "initializing", + }, + }, + { + name: "transition from partial (with status) to complete - accepted", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "initializing", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "ready", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "ready", + }, + }, + { + name: "transition from partial (without status) to complete - accepted", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "ready", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "ready", + }, + }, + { + name: "transition from complete (without status) to partial - rejected", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "initializing", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := applicationProfileStrategy{} + + obj := &softwarecomposition.ApplicationProfile{ObjectMeta: metav1.ObjectMeta{Annotations: tt.newAnnotations}} + old := &softwarecomposition.ApplicationProfile{ObjectMeta: metav1.ObjectMeta{Annotations: tt.oldAnnotations}} + + s.PrepareForUpdate(context.Background(), obj, old) + if !reflect.DeepEqual(obj.Annotations, tt.expected) { + t.Errorf("PrepareForUpdate() = %v, want %v", obj.Annotations, tt.expected) + } + }) + } +} diff --git a/pkg/registry/softwarecomposition/networkneighbors/strategy.go b/pkg/registry/softwarecomposition/networkneighbors/strategy.go index a44073153..f7e701605 100644 --- a/pkg/registry/softwarecomposition/networkneighbors/strategy.go +++ b/pkg/registry/softwarecomposition/networkneighbors/strategy.go @@ -4,7 +4,9 @@ import ( "context" "fmt" + "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" "github.com/kubescape/storage/pkg/apis/softwarecomposition" + "github.com/kubescape/storage/pkg/utils" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -53,11 +55,37 @@ func (networkNeighborsStrategy) NamespaceScoped() bool { func (networkNeighborsStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { } -func (networkNeighborsStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { +func (s networkNeighborsStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + newNN := obj.(*softwarecomposition.NetworkNeighbors) + oldNN := old.(*softwarecomposition.NetworkNeighbors) + + // completion status cannot be transitioned from 'complete' -> 'partial' + // in such case, we reject status updates + if oldNN.Annotations[helpers.CompletionMetadataKey] == helpers.Complete && newNN.Annotations[helpers.CompletionMetadataKey] == helpers.Partial { + newNN.Annotations[helpers.CompletionMetadataKey] = helpers.Complete + + if v, ok := oldNN.Annotations[helpers.StatusMetadataKey]; ok { + newNN.Annotations[helpers.StatusMetadataKey] = v + } else { + delete(newNN.Annotations, helpers.StatusMetadataKey) + } + } } func (networkNeighborsStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return field.ErrorList{} + nn := obj.(*softwarecomposition.NetworkNeighbors) + + allErrors := field.ErrorList{} + + if err := utils.ValidateCompletionAnnotation(nn.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + if err := utils.ValidateStatusAnnotation(nn.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + return allErrors } // WarningsOnCreate returns warnings for the creation of the given object. @@ -77,7 +105,19 @@ func (networkNeighborsStrategy) Canonicalize(obj runtime.Object) { } func (networkNeighborsStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return field.ErrorList{} + nn := obj.(*softwarecomposition.NetworkNeighbors) + + allErrors := field.ErrorList{} + + if err := utils.ValidateCompletionAnnotation(nn.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + if err := utils.ValidateStatusAnnotation(nn.Annotations); err != nil { + allErrors = append(allErrors, err) + } + + return allErrors } // WarningsOnUpdate returns warnings for the given update. diff --git a/pkg/registry/softwarecomposition/networkneighbors/strategy_test.go b/pkg/registry/softwarecomposition/networkneighbors/strategy_test.go new file mode 100644 index 000000000..acdcd3019 --- /dev/null +++ b/pkg/registry/softwarecomposition/networkneighbors/strategy_test.go @@ -0,0 +1,92 @@ +package networkneighbors + +import ( + "context" + "reflect" + "testing" + + "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/kubescape/storage/pkg/apis/softwarecomposition" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPrepareForUpdate(t *testing.T) { + tests := []struct { + name string + oldAnnotations map[string]string + newAnnotations map[string]string + expected map[string]string + }{ + { + name: "transition from complete (with status) to partial - rejected", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "initializing", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "ready", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "initializing", + }, + }, + { + name: "transition from partial (with status) to complete - accepted", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "initializing", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "ready", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "ready", + }, + }, + { + name: "transition from partial (without status) to complete - accepted", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "ready", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "ready", + }, + }, + { + name: "transition from complete (without status) to partial - rejected", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "initializing", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := networkNeighborsStrategy{} + + obj := &softwarecomposition.NetworkNeighbors{ObjectMeta: metav1.ObjectMeta{Annotations: tt.newAnnotations}} + old := &softwarecomposition.NetworkNeighbors{ObjectMeta: metav1.ObjectMeta{Annotations: tt.oldAnnotations}} + + s.PrepareForUpdate(context.Background(), obj, old) + if !reflect.DeepEqual(obj.Annotations, tt.expected) { + t.Errorf("PrepareForUpdate() = %v, want %v", obj.Annotations, tt.expected) + } + }) + } +} diff --git a/pkg/utils/validations.go b/pkg/utils/validations.go new file mode 100644 index 000000000..212b9fb7d --- /dev/null +++ b/pkg/utils/validations.go @@ -0,0 +1,31 @@ +package utils + +import ( + "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateCompletionAnnotation(annotations map[string]string) *field.Error { + if v, ok := annotations[helpers.CompletionMetadataKey]; ok { + switch v { + case helpers.Complete, helpers.Partial: + return nil + default: + return field.Invalid(field.NewPath("metadata").Child("annotations").Child(helpers.CompletionMetadataKey), v, "invalid value") + } + } + return nil +} + +func ValidateStatusAnnotation(annotations map[string]string) *field.Error { + if v, ok := annotations[helpers.StatusMetadataKey]; ok { + switch v { + case helpers.Initializing, helpers.Ready, helpers.Completed, helpers.Incomplete, helpers.Unauthorize, helpers.MissingRuntime, helpers.TooLarge: + return nil + default: + return field.Invalid(field.NewPath("metadata").Child("annotations").Child(helpers.StatusMetadataKey), v, "invalid value") + } + } + + return nil +} diff --git a/pkg/utils/validations_test.go b/pkg/utils/validations_test.go new file mode 100644 index 000000000..b50f5b234 --- /dev/null +++ b/pkg/utils/validations_test.go @@ -0,0 +1,108 @@ +package utils + +import ( + "testing" +) + +func TestValidateStatusAnnotation(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + wantErr bool + }{ + { + name: "valid status - initializing", + annotations: map[string]string{"kubescape.io/status": "initializing"}, + wantErr: false, + }, + { + name: "valid status - ready", + annotations: map[string]string{"kubescape.io/status": "ready"}, + wantErr: false, + }, + { + name: "valid status - completed", + annotations: map[string]string{"kubescape.io/status": "completed"}, + wantErr: false, + }, + { + name: "valid status - incomplete", + annotations: map[string]string{"kubescape.io/status": "incomplete"}, + wantErr: false, + }, + { + name: "valid status - unauthorize", + annotations: map[string]string{"kubescape.io/status": "unauthorize"}, + wantErr: false, + }, + + { + name: "valid status - missing runtime", + annotations: map[string]string{"kubescape.io/status": "missing-runtime"}, + wantErr: false, + }, + + { + name: "valid status - too large", + annotations: map[string]string{"kubescape.io/status": "too-large"}, + wantErr: false, + }, + { + name: "invalid status", + annotations: map[string]string{"kubescape.io/status": "invalid"}, + wantErr: true, + }, + { + name: "no status", + annotations: map[string]string{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateStatusAnnotation(tt.annotations) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateStatusAnnotation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateCompletionAnnotation(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + wantErr bool + }{ + { + name: "valid completion - complete", + annotations: map[string]string{"kubescape.io/completion": "complete"}, + wantErr: false, + }, + { + name: "valid completion - partial", + annotations: map[string]string{"kubescape.io/completion": "partial"}, + wantErr: false, + }, + { + name: "invalid completion", + annotations: map[string]string{"kubescape.io/completion": "invalid"}, + wantErr: true, + }, + { + name: "no completion", + annotations: map[string]string{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateCompletionAnnotation(tt.annotations) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateCompletionAnnotation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}