From 8e9fbd25b3a2bb08e39c00c9fcecadd36afc841b Mon Sep 17 00:00:00 2001 From: Rashmi Gottipati Date: Wed, 8 May 2024 08:00:44 -0400 Subject: [PATCH] Add change validation for maximum constraints to CRD Upgrade safety preflight check (#951) Signed-off-by: Rashmi Gottipati --- pkg/kapp/crdupgradesafety/change_validator.go | 128 +++++++ .../crdupgradesafety/change_validator_test.go | 356 ++++++++++++++++++ pkg/kapp/crdupgradesafety/preflight.go | 4 + ...invalid_field_change_maximum_added_test.go | 115 ++++++ ...lid_field_change_maximum_decreased_test.go | 116 ++++++ ...nvalid_field_change_maxitems_added_test.go | 119 ++++++ ...id_field_change_maxitems_decreased_test.go | 120 ++++++ ...valid_field_change_maxlength_added_test.go | 115 ++++++ ...d_field_change_maxlength_decreased_test.go | 116 ++++++ ...d_field_change_maxproperties_added_test.go | 115 ++++++ ...eld_change_maxproperties_decreased_test.go | 116 ++++++ ...lid_field_change_maximum_increased_test.go | 115 ++++++ ...id_field_change_maxitems_increased_test.go | 119 ++++++ ...d_field_change_maxlength_increased_test.go | 115 ++++++ ...eld_change_maxproperties_increased_test.go | 115 ++++++ 15 files changed, 1884 insertions(+) create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_added_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_decreased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_added_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_decreased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_added_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_decreased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_added_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_decreased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_valid_field_change_maximum_increased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_valid_field_change_maxitems_increased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_valid_field_change_maxlength_increased_test.go create mode 100644 test/e2e/preflight_crdupgradesafety_valid_field_change_maxproperties_increased_test.go diff --git a/pkg/kapp/crdupgradesafety/change_validator.go b/pkg/kapp/crdupgradesafety/change_validator.go index e87d32755..8030fb9ad 100644 --- a/pkg/kapp/crdupgradesafety/change_validator.go +++ b/pkg/kapp/crdupgradesafety/change_validator.go @@ -241,6 +241,134 @@ func MinimumPropertiesChangeValidation(diff FieldDiff) (bool, error) { } } +// MaximumChangeValidation adds a validation check to ensure that +// existing fields can have their maximum constraints updated in a CRD schema +// based on the following: +// - No maximum constraint can be added if one did not exist previously +// - Maximum constraints can not decrease 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 maximum constraints) +// - An error if either of the above criteria are not met +func MaximumChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.Maximum = nil + diff.New.Maximum = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.Maximum == nil && diff.New.Maximum != nil: + m := *diff.New.Maximum + return handled(), fmt.Errorf("maximum constraint added when one did not exist previously: %+v", m) + case diff.Old.Maximum != nil && diff.New.Maximum != nil: + oldMax := *diff.Old.Maximum + newMax := *diff.New.Maximum + if newMax < oldMax { + return handled(), fmt.Errorf("maximum constraint decreased from %+v to %+v", oldMax, newMax) + } + fallthrough + default: + return handled(), nil + } +} + +// MaximumLengthChangeValidation adds a validation check to ensure that +// existing fields can have their maximum length constraints updated in a CRD schema +// based on the following: +// - No maximum length constraint can be added if one did not exist previously +// - Maximum length constraints can not decrease 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 maximum length constraints) +// - An error if either of the above criteria are not met +func MaximumLengthChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.MaxLength = nil + diff.New.MaxLength = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.MaxLength == nil && diff.New.MaxLength != nil: + m := *diff.New.MaxLength + return handled(), fmt.Errorf("maximum length constraint added when one did not exist previously: %+v", m) + case diff.Old.MaxLength != nil && diff.New.MaxLength != nil: + oldMax := *diff.Old.MaxLength + newMax := *diff.New.MaxLength + if newMax < oldMax { + return handled(), fmt.Errorf("maximum length constraint decreased from %+v to %+v", oldMax, newMax) + } + fallthrough + default: + return handled(), nil + } +} + +// MaximumItemsChangeValidation adds a validation check to ensure that +// existing fields can have their maximum item constraints updated in a CRD schema +// based on the following: +// - No maximum item constraint can be added if one did not exist previously +// - Maximum item constraints can not decrease 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 maximum item constraints) +// - An error if either of the above criteria are not met +func MaximumItemsChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.MaxItems = nil + diff.New.MaxItems = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.MaxItems == nil && diff.New.MaxItems != nil: + m := *diff.New.MaxItems + return handled(), fmt.Errorf("maximum items constraint added when one did not exist previously: %+v", m) + case diff.Old.MaxItems != nil && diff.New.MaxItems != nil: + oldMax := *diff.Old.MaxItems + newMax := *diff.New.MaxItems + if newMax < oldMax { + return handled(), fmt.Errorf("maximum items constraint decreased from %+v to %+v", oldMax, newMax) + } + fallthrough + default: + return handled(), nil + } +} + +// MaximumPropertiesChangeValidation adds a validation check to ensure that +// existing fields can have their maximum properties constraints updated in a CRD schema +// based on the following: +// - No maximum properties constraint can be added if one did not exist previously +// - Maximum 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 maximum properties constraints) +// - An error if either of the above criteria are not met +func MaximumPropertiesChangeValidation(diff FieldDiff) (bool, error) { + handled := func() bool { + diff.Old.MaxProperties = nil + diff.New.MaxProperties = nil + return reflect.DeepEqual(diff.Old, diff.New) + } + + switch { + case diff.Old.MaxProperties == nil && diff.New.MaxProperties != nil: + m := *diff.New.MaxProperties + return handled(), fmt.Errorf("maximum properties constraint added when one did not exist previously: %+v", m) + case diff.Old.MaxProperties != nil && diff.New.MaxProperties != nil: + oldMax := *diff.Old.MaxProperties + newMax := *diff.New.MaxProperties + if newMax < oldMax { + return handled(), fmt.Errorf("maximum properties constraint decreased from %+v to %+v", oldMax, newMax) + } + 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 7f67e03cf..f990676d3 100644 --- a/pkg/kapp/crdupgradesafety/change_validator_test.go +++ b/pkg/kapp/crdupgradesafety/change_validator_test.go @@ -865,3 +865,359 @@ func TestMinimumPropertiesChangeValidation(t *testing.T) { }) } } + +func TestMaximumChangeValidation(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{ + Maximum: pointer.Float64(10), + }, + New: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + }, + }, + shouldHandle: true, + }, + { + name: "maximum increased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + }, + New: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(100), + }, + }, + shouldHandle: true, + }, + { + name: "maximum decreased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + }, + New: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(1), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no maximum before, maximum added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "maximum removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no maximum change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + ID: "abc", + }, + New: &v1.JSONSchemaProps{ + Maximum: pointer.Float64(10), + ID: "xyz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MaximumChangeValidation(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.Maximum) + assert.Empty(t, tc.diff.New.Maximum) + }) + } +} + +func TestMaximumLengthChangeValidation(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{ + MaxLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + }, + }, + shouldHandle: true, + }, + { + name: "maximum length increased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(100), + }, + }, + shouldHandle: true, + }, + { + name: "maximum length decreased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(1), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no maximum length before, maximum length added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "maximum length removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no maximum length change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + ID: "abc", + }, + New: &v1.JSONSchemaProps{ + MaxLength: pointer.Int64(10), + ID: "xyz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MaximumLengthChangeValidation(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.MaxLength) + assert.Empty(t, tc.diff.New.MaxLength) + }) + } +} + +func TestMaximumItemsChangeValidation(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{ + MaxItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + }, + }, + shouldHandle: true, + }, + { + name: "maximum items increased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(100), + }, + }, + shouldHandle: true, + }, + { + name: "maximum items decreased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(1), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no maximum items before, maximum items added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "maximum items removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no maximum items change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + ID: "abc", + }, + New: &v1.JSONSchemaProps{ + MaxItems: pointer.Int64(10), + ID: "xyz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MaximumItemsChangeValidation(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.MaxItems) + assert.Empty(t, tc.diff.New.MaxItems) + }) + } +} + +func TestMaximumPropertiesChangeValidation(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{ + MaxProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + }, + }, + shouldHandle: true, + }, + { + name: "maximum properties increased, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(100), + }, + }, + shouldHandle: true, + }, + { + name: "maximum properties decreased, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(1), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "no maximum properties before, maximum properties added, no other changes, error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{}, + New: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + }, + }, + shouldHandle: true, + shouldError: true, + }, + { + name: "maximum properties removed, no other changes, no error, marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + }, + New: &v1.JSONSchemaProps{}, + }, + shouldHandle: true, + }, + { + name: "no maximum properties change, other changes, no error, not marked as handled", + diff: crdupgradesafety.FieldDiff{ + Old: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + ID: "abc", + }, + New: &v1.JSONSchemaProps{ + MaxProperties: pointer.Int64(10), + ID: "xyz", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handled, err := crdupgradesafety.MaximumPropertiesChangeValidation(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.MaxProperties) + assert.Empty(t, tc.diff.New.MaxProperties) + }) + } +} diff --git a/pkg/kapp/crdupgradesafety/preflight.go b/pkg/kapp/crdupgradesafety/preflight.go index 8566571ac..0a166eac8 100644 --- a/pkg/kapp/crdupgradesafety/preflight.go +++ b/pkg/kapp/crdupgradesafety/preflight.go @@ -45,6 +45,10 @@ func NewPreflight(df cmdcore.DepsFactory, enabled bool) *Preflight { MinimumItemsChangeValidation, MinimumLengthChangeValidation, MinimumPropertiesChangeValidation, + MaximumChangeValidation, + MaximumLengthChangeValidation, + MaximumItemsChangeValidation, + MaximumPropertiesChangeValidation, }, }, }, diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_added_test.go new file mode 100644 index 000000000..795ac228d --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaximumAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaximumadded" + + 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: + maximum: 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 maximum constraint for an 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(), "maximum constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_decreased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_decreased_test.go new file mode 100644 index 000000000..f8c16a617 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maximum_decreased_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaximumDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaximumdecreased" + + 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: + maximum: 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: + maximum: 1 + 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 maximum constraint for an 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(), "maximum constraint decreased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_added_test.go new file mode 100644 index 000000000..9f2e4aaf9 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaxItemsAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaxitemsadded" + + 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: + maxItems: 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 maximum items constraint for an 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(), "maximum items constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_decreased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_decreased_test.go new file mode 100644 index 000000000..84cfc81a5 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxitems_decreased_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaxItemsDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaxitemsdecreased" + + 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: + maxItems: 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: + maxItems: 1 + 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 maximum items constraint for an 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(), "maximum items constraint decreased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_added_test.go new file mode 100644 index 000000000..8feff14b1 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaxLengthAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaxlengthadded" + + 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: + maxLength: 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 maximum length constraint for an 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(), "maximum length constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_decreased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_decreased_test.go new file mode 100644 index 000000000..6eae96fb9 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxlength_decreased_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaxLengthDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaxlengthdecreased" + + 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: + maxLength: 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: + maxLength: 1 + 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 maximum length constraint for an 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(), "maximum length constraint decreased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_added_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_added_test.go new file mode 100644 index 000000000..74a640bbc --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaxPropertiesAdded(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaxpropertiesadded" + + 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: + maxProperties: 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 maximum properties constraint for an 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(), "maximum properties constraint added when one did not exist previously") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_decreased_test.go b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_decreased_test.go new file mode 100644 index 000000000..3d1729afa --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_invalid_field_change_maxproperties_decreased_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 TestPreflightCRDUpgradeSafetyInvalidFieldChangeMaxPropertiesDecreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyinvalidfieldchangemaxpropertiesdecreased" + + 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: + maxProperties: 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: + maxProperties: 1 + 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 maximum properties constraint for an 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(), "maximum properties constraint decreased") + }) +} diff --git a/test/e2e/preflight_crdupgradesafety_valid_field_change_maximum_increased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_maximum_increased_test.go new file mode 100644 index 000000000..94111ce02 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_maximum_increased_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 TestPreflightCRDUpgradeSafetyValidFieldChangeMaximumIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangemaximumincreased" + + 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: + maximum: 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: + maximum: 100 + 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 maximum constraint for an 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_maxitems_increased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_maxitems_increased_test.go new file mode 100644 index 000000000..24c1e69c3 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_maxitems_increased_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 TestPreflightCRDUpgradeSafetyValidFieldChangeMaxItemsIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangemaxitemsincreased" + + 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: + maxItems: 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: + maxItems: 100 + 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 maximum items constraint for an 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_maxlength_increased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_maxlength_increased_test.go new file mode 100644 index 000000000..d446ac806 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_maxlength_increased_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 TestPreflightCRDUpgradeSafetyValidFieldChangeMaxLengthIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangemaxlengthincreased" + + 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: + maxLength: 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: + maxLength: 100 + 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 maximum length constraint for an 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_maxproperties_increased_test.go b/test/e2e/preflight_crdupgradesafety_valid_field_change_maxproperties_increased_test.go new file mode 100644 index 000000000..b137819d4 --- /dev/null +++ b/test/e2e/preflight_crdupgradesafety_valid_field_change_maxproperties_increased_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 TestPreflightCRDUpgradeSafetyValidFieldChangeMaxPropertiesIncreased(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + testName := "preflightcrdupgradesafetyvalidfieldchangemaxpropertiesincreased" + + 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: + maxProperties: 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: + maxProperties: 100 + 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 maximum properties constraint for an 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) + }) +}