diff --git a/pkg/apis/softwarecomposition/v1beta1/types.go b/pkg/apis/softwarecomposition/v1beta1/types.go index 1a9f4d8be..7d478cc26 100644 --- a/pkg/apis/softwarecomposition/v1beta1/types.go +++ b/pkg/apis/softwarecomposition/v1beta1/types.go @@ -255,14 +255,14 @@ type ApplicationProfileSpec struct { type ApplicationProfileContainer struct { Name string `json:"name,omitempty"` - Capabilities []string `json:"capabilities,omitempty"` + Capabilities []string `json:"capabilities"` // +patchMergeKey=path // +patchStrategy=merge - Execs []ExecCalls `json:"execs,omitempty" patchStrategy:"merge" patchMergeKey:"path"` + Execs []ExecCalls `json:"execs" patchStrategy:"merge" patchMergeKey:"path"` // +patchMergeKey=path // +patchStrategy=merge - Opens []OpenCalls `json:"opens,omitempty" patchStrategy:"merge" patchMergeKey:"path"` - Syscalls []string `json:"syscalls,omitempty"` + Opens []OpenCalls `json:"opens" patchStrategy:"merge" patchMergeKey:"path"` + Syscalls []string `json:"syscalls"` } type ExecCalls struct { diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index ccb167da9..9a37cc149 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -617,6 +617,7 @@ func schema_pkg_apis_softwarecomposition_v1beta1_ApplicationProfileContainer(ref }, }, }, + Required: []string{"capabilities", "execs", "opens", "syscalls"}, }, }, Dependencies: []string{ diff --git a/pkg/registry/softwarecomposition/applicationprofile/strategy.go b/pkg/registry/softwarecomposition/applicationprofile/strategy.go index c359bed6a..52e29cc33 100644 --- a/pkg/registry/softwarecomposition/applicationprofile/strategy.go +++ b/pkg/registry/softwarecomposition/applicationprofile/strategy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + logHelpers "github.com/kubescape/go-logger/helpers" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -12,6 +13,7 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" + "github.com/kubescape/go-logger" "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" "github.com/kubescape/storage/pkg/apis/softwarecomposition" "github.com/kubescape/storage/pkg/utils" @@ -62,9 +64,22 @@ func (applicationProfileStrategy) PrepareForUpdate(ctx context.Context, obj, old newAP := obj.(*softwarecomposition.ApplicationProfile) oldAP := old.(*softwarecomposition.ApplicationProfile) + // if we have an application profile that is marked as complete and completed, we do not allow any updates + if oldAP.Annotations[helpers.CompletionMetadataKey] == helpers.Complete && oldAP.Annotations[helpers.StatusMetadataKey] == helpers.Completed { + logger.L().Debug("application profile is marked as complete and completed, rejecting update", + logHelpers.String("name", oldAP.Name), + logHelpers.String("namespace", oldAP.Namespace)) + *newAP = *oldAP // reset the new object to the old object + return + } + // 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 { + logger.L().Debug("application profile completion status cannot be transitioned from 'complete' to 'partial', rejecting status updates", + logHelpers.String("name", oldAP.Name), + logHelpers.String("namespace", oldAP.Namespace)) + newAP.Annotations[helpers.CompletionMetadataKey] = helpers.Complete if v, ok := oldAP.Annotations[helpers.StatusMetadataKey]; ok { diff --git a/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go b/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go index 77bb45ac4..7cd5237f5 100644 --- a/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go +++ b/pkg/registry/softwarecomposition/applicationprofile/strategy_test.go @@ -10,7 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestPrepareForUpdate(t *testing.T) { +func TestPrepareForUpdateAnnotations(t *testing.T) { tests := []struct { name string oldAnnotations map[string]string @@ -74,6 +74,21 @@ func TestPrepareForUpdate(t *testing.T) { helpers.CompletionMetadataKey: "complete", }, }, + { + name: "transition from a final AP - all changes are rejected", + oldAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "completed", + }, + newAnnotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "initializing", + }, + expected: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "completed", + }, + }, } for _, tt := range tests { @@ -90,3 +105,164 @@ func TestPrepareForUpdate(t *testing.T) { }) } } + +func TestPrepareForUpdateFullObj(t *testing.T) { + tests := []struct { + name string + old *softwarecomposition.ApplicationProfile + new *softwarecomposition.ApplicationProfile + expected *softwarecomposition.ApplicationProfile + }{ + { + name: "transition from initializing to ready - changes are accepted", + old: &softwarecomposition.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "initializing", + }, + }, + Spec: softwarecomposition.ApplicationProfileSpec{ + Containers: []softwarecomposition.ApplicationProfileContainer{ + { + Name: "container1", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + }, + }, + }, + new: &softwarecomposition.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "ready", + }, + }, + Spec: softwarecomposition.ApplicationProfileSpec{ + Containers: []softwarecomposition.ApplicationProfileContainer{ + { + Name: "container1", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + { + Name: "container2", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + }, + }, + }, + expected: &softwarecomposition.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "ready", + }, + }, + Spec: softwarecomposition.ApplicationProfileSpec{ + Containers: []softwarecomposition.ApplicationProfileContainer{ + { + Name: "container1", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + { + Name: "container2", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + }, + }, + }, + }, + { + name: "transition from a final AP - all changes are rejected", + old: &softwarecomposition.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "completed", + }, + }, + Spec: softwarecomposition.ApplicationProfileSpec{ + Containers: []softwarecomposition.ApplicationProfileContainer{ + { + Name: "container1", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + }, + }, + }, + new: &softwarecomposition.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + helpers.CompletionMetadataKey: "partial", + helpers.StatusMetadataKey: "initializing", + }, + }, + Spec: softwarecomposition.ApplicationProfileSpec{ + Containers: []softwarecomposition.ApplicationProfileContainer{ + { + Name: "container1", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + { + Name: "container2", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + }, + }, + }, + expected: &softwarecomposition.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + helpers.CompletionMetadataKey: "complete", + helpers.StatusMetadataKey: "completed", + }, + }, + Spec: softwarecomposition.ApplicationProfileSpec{ + Containers: []softwarecomposition.ApplicationProfileContainer{ + { + Name: "container1", + Capabilities: []string{}, + Execs: []softwarecomposition.ExecCalls{ + {Path: "/usr/bin/ls", Args: []string{"-l", "/tmp"}}, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := applicationProfileStrategy{} + s.PrepareForUpdate(context.Background(), tt.new, tt.old) + if !reflect.DeepEqual(tt.new, tt.expected) { + t.Errorf("PrepareForUpdate() = %v, want %v", tt.new, tt.expected) + } + }) + } +}