diff --git a/go.mod b/go.mod index 4ee04c56d..fc19c3ea4 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( k8s.io/apimachinery v0.30.0 k8s.io/client-go v0.30.0 k8s.io/component-helpers v0.29.3 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/yaml v1.4.0 ) @@ -67,7 +68,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/pkg/kapp/crdupgradesafety/change_validator.go b/pkg/kapp/crdupgradesafety/change_validator.go index f346a0549..e87d32755 100644 --- a/pkg/kapp/crdupgradesafety/change_validator.go +++ b/pkg/kapp/crdupgradesafety/change_validator.go @@ -113,6 +113,134 @@ func RequiredFieldChangeValidation(diff FieldDiff) (bool, error) { return handled(), nil } +// MinimumChangeValidation adds a validation check to ensure that +// existing fields can have their minimum constraints updated in a CRD schema +// based on the following: +// - No minimum constraint can be added if one did not exist previously +// - Minimum constraints can not increase in value +// This function returns: +// - A boolean representation of whether or not the change +// has been fully handled (i.e. the only change was to minimum constraints) +// - An error if either of the above criteria are not met +func MinimumChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.Minimum = nil + diff.New.Minimum = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.Minimum == nil && diff.New.Minimum != nil: + m := *diff.New.Minimum + return handled(), fmt.Errorf("minimum constraint added when one did not exist previously: %+v", m) + case diff.Old.Minimum != nil && diff.New.Minimum != nil: + oldMin := *diff.Old.Minimum + newMin := *diff.New.Minimum + if oldMin < newMin { + return handled(), fmt.Errorf("minimum constraint increased from %+v to %+v", oldMin, newMin) + } + fallthrough + default: + return handled(), nil + } +} + +// MinimumLengthChangeValidation adds a validation check to ensure that +// existing fields can have their minimum length constraints updated in a CRD schema +// based on the following: +// - No minimum length constraint can be added if one did not exist previously +// - Minimum length constraints can not increase in value +// This function returns: +// - A boolean representation of whether or not the change +// has been fully handled (i.e. the only change was to minimum length constraints) +// - An error if either of the above criteria are not met +func MinimumLengthChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.MinLength = nil + diff.New.MinLength = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.MinLength == nil && diff.New.MinLength != nil: + m := *diff.New.MinLength + return handled(), fmt.Errorf("minimum length constraint added when one did not exist previously: %+v", m) + case diff.Old.MinLength != nil && diff.New.MinLength != nil: + oldMin := *diff.Old.MinLength + newMin := *diff.New.MinLength + if oldMin < newMin { + return handled(), fmt.Errorf("minimum length constraint increased from %+v to %+v", oldMin, newMin) + } + fallthrough + default: + return handled(), nil + } +} + +// MinimumItemsChangeValidation adds a validation check to ensure that +// existing fields can have their minimum item constraints updated in a CRD schema +// based on the following: +// - No minimum item constraint can be added if one did not exist previously +// - Minimum item constraints can not increase in value +// This function returns: +// - A boolean representation of whether or not the change +// has been fully handled (i.e. the only change was to minimum item constraints) +// - An error if either of the above criteria are not met +func MinimumItemsChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.MinItems = nil + diff.New.MinItems = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.MinItems == nil && diff.New.MinItems != nil: + m := *diff.New.MinItems + return handled(), fmt.Errorf("minimum items constraint added when one did not exist previously: %+v", m) + case diff.Old.MinItems != nil && diff.New.MinItems != nil: + oldMin := *diff.Old.MinItems + newMin := *diff.New.MinItems + if oldMin < newMin { + return handled(), fmt.Errorf("minimum items constraint increased from %+v to %+v", oldMin, newMin) + } + fallthrough + default: + return handled(), nil + } +} + +// MinimumPropertiesChangeValidation adds a validation check to ensure that +// existing fields can have their minimum properties constraints updated in a CRD schema +// based on the following: +// - No minimum properties constraint can be added if one did not exist previously +// - Minimum properties constraints can not increase in value +// This function returns: +// - A boolean representation of whether or not the change +// has been fully handled (i.e. the only change was to minimum properties constraints) +// - An error if either of the above criteria are not met +func MinimumPropertiesChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.MinProperties = nil + diff.New.MinProperties = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.MinProperties == nil && diff.New.MinProperties != nil: + m := *diff.New.MinProperties + return handled(), fmt.Errorf("minimum properties constraint added when one did not exist previously: %+v", m) + case diff.Old.MinProperties != nil && diff.New.MinProperties != nil: + oldMin := *diff.Old.MinProperties + newMin := *diff.New.MinProperties + if oldMin < newMin { + return handled(), fmt.Errorf("minimum properties constraint increased from %+v to %+v", oldMin, newMin) + } + fallthrough + default: + return handled(), nil + } +} + // ChangeValidator is a Validation implementation focused on // handling updates to existing fields in a CRD type ChangeValidator struct { diff --git a/pkg/kapp/crdupgradesafety/change_validator_test.go b/pkg/kapp/crdupgradesafety/change_validator_test.go index c787ba7a8..7f67e03cf 100644 --- a/pkg/kapp/crdupgradesafety/change_validator_test.go +++ b/pkg/kapp/crdupgradesafety/change_validator_test.go @@ -10,6 +10,7 @@ import ( "carvel.dev/kapp/pkg/kapp/crdupgradesafety" "github.com/stretchr/testify/assert" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/utils/pointer" ) func TestEnumChangeValidation(t *testing.T) { @@ -508,3 +509,359 @@ func TestRequiredFieldChangeValidation(t *testing.T) { }) } } + +func TestMinimumChangeValidation(t *testing.T) { + for _, tc := range []struct { + name string + diff crdupgradesafety.FieldDiff + shouldError bool + shouldHandle bool + }{ + { + name: "no change, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(10), + }, + New: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(10), + }, + }, + shouldHandle: true, + }, + { + name: "minimum decreased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(10), + }, + New: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(8), + }, + }, + shouldHandle: true, + }, + { + name: "minimum increased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(8), + }, + New: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no minimum before, minimum added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(8), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "minimum removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(8), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no minimum change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(8), + ID: "bar", + }, + New: &v1.JSONSchemaProps{ + Minimum: pointer.Float64(8), + ID: "baz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MinimumChangeValidation(tc.diff) + assert.Equal(t, tc.shouldError, err != nil, "should error? - %v", tc.shouldError) + assert.Equal(t, tc.shouldHandle, handled, "should be handled? - %v", tc.shouldHandle) + assert.Empty(t, tc.diff.Old.Minimum) + assert.Empty(t, tc.diff.New.Minimum) + }) + } +} + +func TestMinimumLengthChangeValidation(t *testing.T) { + for _, tc := range []struct { + name string + diff crdupgradesafety.FieldDiff + shouldError bool + shouldHandle bool + }{ + { + name: "no change, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + }, + }, + shouldHandle: true, + }, + { + name: "minimum length decreased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(8), + }, + }, + shouldHandle: true, + }, + { + name: "minimum length increased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(8), + }, + New: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no minimum length before, minimum length added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "minimum length removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no minimum length change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + ID: "bar", + }, + New: &v1.JSONSchemaProps{ + MinLength: pointer.Int64(10), + ID: "baz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MinimumLengthChangeValidation(tc.diff) + assert.Equal(t, tc.shouldError, err != nil, "should error? - %v", tc.shouldError) + assert.Equal(t, tc.shouldHandle, handled, "should be handled? - %v", tc.shouldHandle) + assert.Empty(t, tc.diff.Old.MinLength) + assert.Empty(t, tc.diff.New.MinLength) + }) + } +} + +func TestMinimumItemsChangeValidation(t *testing.T) { + for _, tc := range []struct { + name string + diff crdupgradesafety.FieldDiff + shouldError bool + shouldHandle bool + }{ + { + name: "no change, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + }, + }, + shouldHandle: true, + }, + { + name: "minimum items decreased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(8), + }, + }, + shouldHandle: true, + }, + { + name: "minimum items increased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(8), + }, + New: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no minimum items before, minimum items added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "minimum items removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no minimum items change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + ID: "bar", + }, + New: &v1.JSONSchemaProps{ + MinItems: pointer.Int64(10), + ID: "baz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MinimumItemsChangeValidation(tc.diff) + assert.Equal(t, tc.shouldError, err != nil, "should error? - %v", tc.shouldError) + assert.Equal(t, tc.shouldHandle, handled, "should be handled? - %v", tc.shouldHandle) + assert.Empty(t, tc.diff.Old.MinItems) + assert.Empty(t, tc.diff.New.MinItems) + }) + } +} + +func TestMinimumPropertiesChangeValidation(t *testing.T) { + for _, tc := range []struct { + name string + diff crdupgradesafety.FieldDiff + shouldError bool + shouldHandle bool + }{ + { + name: "no change, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + }, + }, + shouldHandle: true, + }, + { + name: "minimum properties decreased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(8), + }, + }, + shouldHandle: true, + }, + { + name: "minimum properties increased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(8), + }, + New: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no minimum properties before, minimum properties added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "minimum properties removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no minimum properties change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + ID: "bar", + }, + New: &v1.JSONSchemaProps{ + MinProperties: pointer.Int64(10), + ID: "baz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MinimumPropertiesChangeValidation(tc.diff) + assert.Equal(t, tc.shouldError, err != nil, "should error? - %v", tc.shouldError) + assert.Equal(t, tc.shouldHandle, handled, "should be handled? - %v", tc.shouldHandle) + assert.Empty(t, tc.diff.Old.MinProperties) + assert.Empty(t, tc.diff.New.MinProperties) + }) + } +} diff --git a/pkg/kapp/crdupgradesafety/preflight.go b/pkg/kapp/crdupgradesafety/preflight.go index 72e3aab70..8566571ac 100644 --- a/pkg/kapp/crdupgradesafety/preflight.go +++ b/pkg/kapp/crdupgradesafety/preflight.go @@ -41,6 +41,10 @@ func NewPreflight(df cmdcore.DepsFactory, enabled bool) *Preflight { Validations: []ChangeValidation{ EnumChangeValidation, RequiredFieldChangeValidation, + MinimumChangeValidation, + MinimumItemsChangeValidation, + MinimumLengthChangeValidation, + MinimumPropertiesChangeValidation, }, }, }, diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minimum_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minimum_added_test.go new file mode 100644 index 000000000..f95b31b76 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minimum_added_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinimumAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminimumadded" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: integer + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minimum: 10 + type: integer + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that adds minimum constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minimum_increased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minimum_increased_test.go new file mode 100644 index 000000000..73876c2f9 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minimum_increased_test.go @@ -0,0 +1,116 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinimumIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminimumincreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minimum: 5 + type: integer + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minimum: 10 + type: integer + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that increases minimum constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum constraint increased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minitems_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minitems_added_test.go new file mode 100644 index 000000000..7f094904b --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minitems_added_test.go @@ -0,0 +1,119 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinItemsAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminitemsadded" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + items: + type: object + type: array + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minItems: 10 + items: + type: object + type: array + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that adds minimum items constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum items constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minitems_increased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minitems_increased_test.go new file mode 100644 index 000000000..755b36dc3 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minitems_increased_test.go @@ -0,0 +1,120 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinItemsIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminitemsincreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minItems: 5 + items: + type: object + type: array + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minItems: 10 + items: + type: object + type: array + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that increases minimum items constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum items constraint increased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minlength_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minlength_added_test.go new file mode 100644 index 000000000..ba75bfc21 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minlength_added_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinLengthAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminlengthadded" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: string + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minLength: 10 + type: string + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that adds minimum length constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum length constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minlength_increased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minlength_increased_test.go new file mode 100644 index 000000000..bf9299dea --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minlength_increased_test.go @@ -0,0 +1,116 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinLengthIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminlengthincreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minLength: 5 + type: string + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minLength: 10 + type: string + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that increases minimum length constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum length constraint increased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minproperties_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minproperties_added_test.go new file mode 100644 index 000000000..622ae926c --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minproperties_added_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinPropertiesAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminpropertiesadded" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minProperties: 10 + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that adds minimum properties constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum properties constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_minproperties_increased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minproperties_increased_test.go new file mode 100644 index 000000000..74204b029 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_minproperties_increased_test.go @@ -0,0 +1,116 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyInvalidFieldChangeMinPropertiesIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangeminpropertiesincreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minProperties: 5 + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minProperties: 10 + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that increases minimum properties constraint for existing field, preflight check enabled, should error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update), AllowError: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "minimum properties constraint increased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_valid_field_change_minimum_decreased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_minimum_decreased_test.go new file mode 100644 index 000000000..77cc48b68 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_minimum_decreased_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyValidFieldChangeMinimumDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangeminimumdecreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minimum: 10 + type: integer + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minimum: 5 + type: integer + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that decreases minimum constraint for existing field, preflight check enabled, should not error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update)}) + require.NoError(t, err) + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_valid_field_change_minitems_decreased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_minitems_decreased_test.go new file mode 100644 index 000000000..9fa143521 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_minitems_decreased_test.go @@ -0,0 +1,119 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyValidFieldChangeMinItemsDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangeminitemsdecreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minItems: 10 + items: + type: object + type: array + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minItems: 5 + items: + type: object + type: array + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that decreases minimum items constraint for existing field, preflight check enabled, should not error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update)}) + require.NoError(t, err) + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_valid_field_change_minlength_decreased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_minlength_decreased_test.go new file mode 100644 index 000000000..2ac39f662 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_minlength_decreased_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyValidFieldChangeMinLengthDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangeminlengthdecreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minLength: 10 + type: string + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minLength: 5 + type: string + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that decreases minimum length constraint for existing field, preflight check enabled, should not error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update)}) + require.NoError(t, err) + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_valid_field_change_minproperties_decreased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_minproperties_decreased_test.go new file mode 100644 index 000000000..3d177d7a0 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_minproperties_decreased_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPreflightCRDUpgradeSafetyValidFieldChangeMinPropertiesDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangeminpropertiesdecreased" + + base := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minProperties: 10 + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + base = strings.ReplaceAll(base, "__test-name__", testName) + appName := "preflight-crdupgradesafety-app" + + cleanUp := func() { + kapp.Run([]string{"delete", "-a", appName}) + } + cleanUp() + defer cleanUp() + + update := ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: memcacheds.__test-name__.example.com +spec: + group: __test-name__.example.com + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + minProperties: 5 + type: object + status: + type: object + type: object + served: true + storage: true + subresources: + status: {} +` + + update = strings.ReplaceAll(update, "__test-name__", testName) + logger.Section("deploy app with CRD update that decreases minimum properties constraint for existing field, preflight check enabled, should not error", func() { + _, err := kapp.RunWithOpts([]string{"deploy", "-a", appName, "-f", "-"}, RunOpts{StdinReader: strings.NewReader(base)}) + require.NoError(t, err) + _, err = kapp.RunWithOpts([]string{"deploy", "--preflight=CRDUpgradeSafety", "-a", appName, "-f", "-"}, + RunOpts{StdinReader: strings.NewReader(update)}) + require.NoError(t, err) + }) +}