From ea516f7ee8b031344c5a89e7d933278f902d1026 Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Wed, 28 Jan 2026 16:32:30 -0600 Subject: [PATCH 01/10] add feature exemption lists --- pkg/admin/prerun/featuregate_lock.go | 6 +++--- pkg/admin/prerun/featuregate_lock_test.go | 22 +++++++++++----------- pkg/config/apiserver.go | 13 ++++++++++--- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/admin/prerun/featuregate_lock.go b/pkg/admin/prerun/featuregate_lock.go index 40e84bd995..07211e9ef8 100644 --- a/pkg/admin/prerun/featuregate_lock.go +++ b/pkg/admin/prerun/featuregate_lock.go @@ -22,9 +22,9 @@ var ( // featureGateLockFile represents the structure of the lock file // that tracks custom feature gate configuration and prevents changes/upgrades type featureGateLockFile struct { - FeatureSet string `json:"featureSet"` - CustomNoUpgrade config.CustomNoUpgrade `json:"customNoUpgrade"` - Version versionMetadata `json:"version"` + FeatureSet string `json:"featureSet"` + CustomNoUpgrade config.EnableDisableFeatures `json:"customNoUpgrade"` + Version versionMetadata `json:"version"` } // FeatureGateLockManagement manages the feature gate lock file diff --git a/pkg/admin/prerun/featuregate_lock_test.go b/pkg/admin/prerun/featuregate_lock_test.go index 1a9a243dd9..f2ee984b07 100644 --- a/pkg/admin/prerun/featuregate_lock_test.go +++ b/pkg/admin/prerun/featuregate_lock_test.go @@ -21,7 +21,7 @@ func TestFeatureGateLockFile_Marshal(t *testing.T) { name: "custom feature gates with enabled and disabled", lockFile: featureGateLockFile{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA", "FeatureB"}, Disabled: []string{"FeatureC"}, }, @@ -32,7 +32,7 @@ func TestFeatureGateLockFile_Marshal(t *testing.T) { name: "TechPreviewNoUpgrade", lockFile: featureGateLockFile{ FeatureSet: config.FeatureSetTechPreviewNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{}, + CustomNoUpgrade: config.EnableDisableFeatures{}, }, wantErr: false, }, @@ -40,7 +40,7 @@ func TestFeatureGateLockFile_Marshal(t *testing.T) { name: "DevPreviewNoUpgrade", lockFile: featureGateLockFile{ FeatureSet: config.FeatureSetDevPreviewNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{}, + CustomNoUpgrade: config.EnableDisableFeatures{}, }, wantErr: false, }, @@ -48,7 +48,7 @@ func TestFeatureGateLockFile_Marshal(t *testing.T) { name: "empty feature gates", lockFile: featureGateLockFile{ FeatureSet: "", - CustomNoUpgrade: config.CustomNoUpgrade{}, + CustomNoUpgrade: config.EnableDisableFeatures{}, }, wantErr: false, }, @@ -85,7 +85,7 @@ func TestIsCustomFeatureGatesConfigured(t *testing.T) { name: "CustomNoUpgrade with enabled features", fg: config.FeatureGates{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA"}, }, }, @@ -116,7 +116,7 @@ func TestIsCustomFeatureGatesConfigured(t *testing.T) { name: "CustomNoUpgrade without any enabled/disabled", fg: config.FeatureGates{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{}, + CustomNoUpgrade: config.EnableDisableFeatures{}, }, want: false, }, @@ -156,7 +156,7 @@ func TestFeatureGateLockFile_ReadWrite(t *testing.T) { name: "write and read custom feature gates", lockFile: featureGateLockFile{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA", "FeatureB"}, Disabled: []string{"FeatureC"}, }, @@ -277,7 +277,7 @@ func TestFeatureGateLockManagement_FirstRun(t *testing.T) { ApiServer: config.ApiServer{ FeatureGates: config.FeatureGates{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA"}, }, }, @@ -326,7 +326,7 @@ func TestFeatureGateLockManagement_ConfigChange(t *testing.T) { // Create lockFile file with initial config (CustomNoUpgrade feature set) initialLock := featureGateLockFile{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA"}, }, Version: testVersion, @@ -439,7 +439,7 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { // Create lockFile file with locked version lockFile := featureGateLockFile{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA"}, }, Version: tt.lockFileVer, @@ -452,7 +452,7 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { ApiServer: config.ApiServer{ FeatureGates: config.FeatureGates{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.CustomNoUpgrade{ + CustomNoUpgrade: config.EnableDisableFeatures{ Enabled: []string{"FeatureA"}, }, }, diff --git a/pkg/config/apiserver.go b/pkg/config/apiserver.go index e8235988ff..1ce3b2dbd7 100644 --- a/pkg/config/apiserver.go +++ b/pkg/config/apiserver.go @@ -139,7 +139,7 @@ const ( FeatureSetDevPreviewNoUpgrade = "DevPreviewNoUpgrade" ) -type CustomNoUpgrade struct { +type EnableDisableFeatures struct { Enabled []string `json:"enabled"` Disabled []string `json:"disabled"` } @@ -149,8 +149,13 @@ type CustomNoUpgrade struct { var RequiredFeatureGates = []string{"UserNamespacesSupport", "UserNamespacesPodSecurityStandards"} type FeatureGates struct { - FeatureSet string `json:"featureSet"` - CustomNoUpgrade CustomNoUpgrade `json:"customNoUpgrade"` + FeatureSet string `json:"featureSet"` + // CustomNoUpgrade is used to enable/disable feature gates. When the enabled or disable lists are not empty, x- and y-stream upgrades will be blocked. + // Use this field exclusively for custom feature gates, unless you are certain that the feature gate is a SpecialHandlingSupportExceptionRequired feature. + CustomNoUpgrade EnableDisableFeatures `json:"customNoUpgrade"` + // SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades. + // A SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features. + SpecialHandlingSupportExceptionRequired EnableDisableFeatures `json:"specialHandlingSupportExceptionRequired"` } // ToApiserverArgs converts the FeatureGates struct to a list of feature-gates arguments for the kube-apiserver. @@ -165,6 +170,8 @@ func (fg FeatureGates) ToApiserverArgs() ([]string, error) { addFeatures(fg.CustomNoUpgrade.Enabled, true) addFeatures(fg.CustomNoUpgrade.Disabled, false) + addFeatures(fg.SpecialHandlingSupportExceptionRequired.Enabled, true) + addFeatures(fg.SpecialHandlingSupportExceptionRequired.Disabled, false) return ret.List(), nil } From 692c08b37eeebbd1dc9b13eecd2c96af1760793b Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Mon, 2 Feb 2026 16:53:26 -0600 Subject: [PATCH 02/10] add prerun logic --- pkg/admin/prerun/featuregate_lock.go | 77 ++++++++++++++--------- pkg/admin/prerun/featuregate_lock_test.go | 4 +- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/pkg/admin/prerun/featuregate_lock.go b/pkg/admin/prerun/featuregate_lock.go index 07211e9ef8..b938c1d49b 100644 --- a/pkg/admin/prerun/featuregate_lock.go +++ b/pkg/admin/prerun/featuregate_lock.go @@ -8,6 +8,7 @@ import ( "github.com/openshift/microshift/pkg/config" "github.com/openshift/microshift/pkg/util" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" "sigs.k8s.io/yaml" ) @@ -31,7 +32,9 @@ type featureGateLockFile struct { // that prevents upgrades and config changes when custom feature gates are configured func FeatureGateLockManagement(cfg *config.Config) error { klog.InfoS("START feature gate lock management") - if err := featureGateLockManagement(cfg); err != nil { + + fgCfg := &cfg.ApiServer.FeatureGates + if err := featureGateLockManagement(fgCfg); err != nil { klog.ErrorS(err, "FAIL feature gate lock management") return err } @@ -39,7 +42,7 @@ func FeatureGateLockManagement(cfg *config.Config) error { return nil } -func featureGateLockManagement(cfg *config.Config) error { +func featureGateLockManagement(fgCfg *config.FeatureGates) error { // If a lock file exists, it must be validated regardless of current config // This prevents users from removing feature gates from config in order to block upgrades and configuration changes lockExists, err := util.PathExists(featureGateLockFilePath) @@ -48,18 +51,18 @@ func featureGateLockManagement(cfg *config.Config) error { } // Lock file exists - validate configuration if lockExists { - return runValidationsChecks(cfg) + return runValidationsChecks(fgCfg) } // No lock file exists yet and custom feature gates are configured, so this is the first time configuring custom feature gates - if cfg.ApiServer.FeatureGates.FeatureSet != "" { - return createFeatureGateLockFile(cfg) + if fgCfg.FeatureSet != "" { + return createFeatureGateLockFile(fgCfg) } // No lock file and no custom feature gates - normal operation return nil } // createFeatureGateLockFile creates the lock file with current configuration -func createFeatureGateLockFile(cfg *config.Config) error { +func createFeatureGateLockFile(fgCfg *config.FeatureGates) error { klog.InfoS("Creating feature gate lock file - this cluster can no longer be upgraded", "path", featureGateLockFilePath) @@ -70,8 +73,8 @@ func createFeatureGateLockFile(cfg *config.Config) error { } lockFile := featureGateLockFile{ - FeatureSet: cfg.ApiServer.FeatureGates.FeatureSet, - CustomNoUpgrade: cfg.ApiServer.FeatureGates.CustomNoUpgrade, + FeatureSet: fgCfg.FeatureSet, + CustomNoUpgrade: fgCfg.CustomNoUpgrade, Version: currentVersion, } @@ -88,7 +91,7 @@ func createFeatureGateLockFile(cfg *config.Config) error { // runValidationsChecks validates the feature gate lock file and the current configuration // It returns an error if the configuration is invalid or if an x or y stream version upgrade has occurred. -func runValidationsChecks(cfg *config.Config) error { +func runValidationsChecks(fgCfg *config.FeatureGates) error { klog.InfoS("Validating feature gate lock file", "path", featureGateLockFilePath) lockFile, err := readFeatureGateLockFile(featureGateLockFilePath) @@ -97,7 +100,7 @@ func runValidationsChecks(cfg *config.Config) error { } // Check if feature gate configuration has changed - if err := configValidationChecksPass(lockFile, cfg.ApiServer.FeatureGates); err != nil { + if err := configValidationChecksPass(lockFile, fgCfg); err != nil { return fmt.Errorf("detected invalid changes in feature gate configuration: %w\n\n"+ "To restore MicroShift to a supported state, you must:\n"+ "1. Run: sudo microshift-cleanup-data --all\n"+ @@ -105,37 +108,55 @@ func runValidationsChecks(cfg *config.Config) error { "3. Restart MicroShift: sudo systemctl restart microshift", err) } - // Check if version has changed (upgrade attempted) + if err := upgradeChecksPass(lockFile, fgCfg); err != nil { + return err + } + + klog.InfoS("Feature gate lock file validation successful") + return nil +} + +func configValidationChecksPass(prev featureGateLockFile, fgCfg *config.FeatureGates) error { + if prev.FeatureSet != "" && fgCfg.FeatureSet == "" { + // Disallow changing from feature set to no feature set + return fmt.Errorf("cannot unset feature set. Previous config had feature set %q, current config has no feature set configured", prev.FeatureSet) + } + if prev.FeatureSet == config.FeatureSetCustomNoUpgrade && fgCfg.FeatureSet != config.FeatureSetCustomNoUpgrade { + // Disallow changing from custom feature gates to any other feature set + return fmt.Errorf("cannot change CustomNoUpgrade feature set. Previous feature set was %q, current feature set is %q", prev.FeatureSet, fgCfg.FeatureSet) + } + return nil +} + +func upgradeChecksPass(lockFile featureGateLockFile, fgCfg *config.FeatureGates) error { currentExecutableVersion, err := getExecutableVersion() if err != nil { return fmt.Errorf("failed to get current executable version: %w", err) } - + featureGatesNoExemptionsEnabled := sets.New[string]() + featureGatesNoExemptionsDisabled := sets.New[string]() if lockFile.Version.Major != currentExecutableVersion.Major || lockFile.Version.Minor != currentExecutableVersion.Minor { + customFgEnabled := sets.New(fgCfg.CustomNoUpgrade.Enabled...) + customFgDisabled := sets.New(fgCfg.CustomNoUpgrade.Disabled...) + specialHandlingFgEnabled := sets.New(fgCfg.SpecialHandlingSupportExceptionRequired.Enabled...) + specialHandlingFgDisabled := sets.New(fgCfg.SpecialHandlingSupportExceptionRequired.Disabled...) + + featureGatesNoExemptionsEnabled.Insert(customFgEnabled.Difference(specialHandlingFgEnabled).UnsortedList()...) + featureGatesNoExemptionsDisabled.Insert(customFgDisabled.Difference(specialHandlingFgDisabled).UnsortedList()...) + return fmt.Errorf("version upgrade detected with custom feature gates: locked version %s, current version %s\n\n"+ "Upgrades are not supported when custom feature gates are configured.\n"+ - "Custom feature gates (%s) were configured in version %s.\n"+ + "Custom feature gates were configured in version %s.\n"+ + "Gates Enabled: %s\n"+ + "Gates Disabled: %s\n"+ "To restore MicroShift to a supported state, you must:\n"+ "1. Roll back to version %s, OR\n"+ "2. Run: sudo microshift-cleanup-data --all\n"+ "3. Remove custom feature gates from /etc/microshift/config.yaml\n"+ "4. Restart MicroShift: sudo systemctl restart microshift", lockFile.Version.String(), currentExecutableVersion.String(), - lockFile.FeatureSet, lockFile.Version.String(), lockFile.Version.String()) - } - - klog.InfoS("Feature gate lock file validation successful") - return nil -} - -func configValidationChecksPass(prev featureGateLockFile, current config.FeatureGates) error { - if prev.FeatureSet != "" && current.FeatureSet == "" { - // Disallow changing from feature set to no feature set - return fmt.Errorf("cannot unset feature set. Previous config had feature set %q, current config has no feature set configured", prev.FeatureSet) - } - if prev.FeatureSet == config.FeatureSetCustomNoUpgrade && current.FeatureSet != config.FeatureSetCustomNoUpgrade { - // Disallow changing from custom feature gates to any other feature set - return fmt.Errorf("cannot change CustomNoUpgrade feature set. Previous feature set was %q, current feature set is %q", prev.FeatureSet, current.FeatureSet) + lockFile.Version.String(), featureGatesNoExemptionsEnabled.UnsortedList(), + featureGatesNoExemptionsDisabled.UnsortedList(), lockFile.Version.String()) } return nil } diff --git a/pkg/admin/prerun/featuregate_lock_test.go b/pkg/admin/prerun/featuregate_lock_test.go index f2ee984b07..12dfc9673e 100644 --- a/pkg/admin/prerun/featuregate_lock_test.go +++ b/pkg/admin/prerun/featuregate_lock_test.go @@ -127,7 +127,7 @@ func TestIsCustomFeatureGatesConfigured(t *testing.T) { err := configValidationChecksPass(featureGateLockFile{ FeatureSet: tt.fg.FeatureSet, CustomNoUpgrade: tt.fg.CustomNoUpgrade, - }, tt.fg) + }, &tt.fg) if err != nil { t.Errorf("featureValidationsPass() error = %v", err) } @@ -242,7 +242,7 @@ func TestConfigValidationChecksPass(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := configValidationChecksPass(tt.lockFile, tt.current) + err := configValidationChecksPass(tt.lockFile, &tt.current) if (err != nil) != tt.wantErr { t.Errorf("configValidationChecksPass() error = %v, wantErr %v", err, tt.wantErr) } From 7c7b0668e445a6812e2f077a2f24d84fd501068d Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Wed, 4 Feb 2026 15:09:48 -0600 Subject: [PATCH 03/10] refactor upgradeCheckPass, update config --- .../config/config-openapi-spec.json | 26 ++++++++++++++++++- docs/user/howto_config.md | 6 +++++ packaging/microshift/config.yaml | 7 +++++ pkg/admin/prerun/featuregate_lock.go | 25 +++++++++--------- pkg/config/apiserver.go | 2 -- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/cmd/generate-config/config/config-openapi-spec.json b/cmd/generate-config/config/config-openapi-spec.json index d615ba026c..c4deb12600 100755 --- a/cmd/generate-config/config/config-openapi-spec.json +++ b/cmd/generate-config/config/config-openapi-spec.json @@ -64,10 +64,12 @@ "type": "object", "required": [ "customNoUpgrade", - "featureSet" + "featureSet", + "specialHandlingSupportExceptionRequired" ], "properties": { "customNoUpgrade": { + "description": "CustomNoUpgrade is used to enable/disable feature gates. When the enabled or disable lists are not empty, x- and y-stream upgrades will be blocked.\nUse this field exclusively for custom feature gates, unless you are certain that the feature gate is a SpecialHandlingSupportExceptionRequired feature.", "type": "object", "required": [ "disabled", @@ -90,6 +92,28 @@ }, "featureSet": { "type": "string" + }, + "specialHandlingSupportExceptionRequired": { + "description": "SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades.\nA SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features.", + "type": "object", + "required": [ + "disabled", + "enabled" + ], + "properties": { + "disabled": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "array", + "items": { + "type": "string" + } + } + } } } }, diff --git a/docs/user/howto_config.md b/docs/user/howto_config.md index 1ae5bf9927..3703ce1919 100644 --- a/docs/user/howto_config.md +++ b/docs/user/howto_config.md @@ -19,6 +19,9 @@ apiServer: disabled: [] enabled: [] featureSet: "" + specialHandlingSupportExceptionRequired: + disabled: [] + enabled: [] namedCertificates: - certPath: "" keyPath: "" @@ -168,6 +171,9 @@ apiServer: disabled: [] enabled: [] featureSet: "" + specialHandlingSupportExceptionRequired: + disabled: [] + enabled: [] namedCertificates: - certPath: "" keyPath: "" diff --git a/packaging/microshift/config.yaml b/packaging/microshift/config.yaml index 7a20f173ca..5a6147e034 100644 --- a/packaging/microshift/config.yaml +++ b/packaging/microshift/config.yaml @@ -15,10 +15,17 @@ apiServer: # profile is the OpenShift profile specifying a specific logging policy profile: Default featureGates: + # CustomNoUpgrade is used to enable/disable feature gates. When the enabled or disable lists are not empty, x- and y-stream upgrades will be blocked. + # Use this field exclusively for custom feature gates, unless you are certain that the feature gate is a SpecialHandlingSupportExceptionRequired feature. customNoUpgrade: disabled: [] enabled: [] featureSet: "" + # SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades. + # A SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features. + specialHandlingSupportExceptionRequired: + disabled: [] + enabled: [] # List of custom certificates used to secure requests to specific host names namedCertificates: - certPath: "" diff --git a/pkg/admin/prerun/featuregate_lock.go b/pkg/admin/prerun/featuregate_lock.go index b938c1d49b..b48c522d55 100644 --- a/pkg/admin/prerun/featuregate_lock.go +++ b/pkg/admin/prerun/featuregate_lock.go @@ -130,19 +130,20 @@ func configValidationChecksPass(prev featureGateLockFile, fgCfg *config.FeatureG func upgradeChecksPass(lockFile featureGateLockFile, fgCfg *config.FeatureGates) error { currentExecutableVersion, err := getExecutableVersion() + lockedVersion := lockFile.Version if err != nil { return fmt.Errorf("failed to get current executable version: %w", err) } - featureGatesNoExemptionsEnabled := sets.New[string]() - featureGatesNoExemptionsDisabled := sets.New[string]() - if lockFile.Version.Major != currentExecutableVersion.Major || lockFile.Version.Minor != currentExecutableVersion.Minor { - customFgEnabled := sets.New(fgCfg.CustomNoUpgrade.Enabled...) - customFgDisabled := sets.New(fgCfg.CustomNoUpgrade.Disabled...) - specialHandlingFgEnabled := sets.New(fgCfg.SpecialHandlingSupportExceptionRequired.Enabled...) - specialHandlingFgDisabled := sets.New(fgCfg.SpecialHandlingSupportExceptionRequired.Disabled...) - featureGatesNoExemptionsEnabled.Insert(customFgEnabled.Difference(specialHandlingFgEnabled).UnsortedList()...) - featureGatesNoExemptionsDisabled.Insert(customFgDisabled.Difference(specialHandlingFgDisabled).UnsortedList()...) + if lockedVersion.Major != currentExecutableVersion.Major || lockedVersion.Minor != currentExecutableVersion.Minor { + extractFeatureGatesWithoutExemptions := func(lhs []string, rhs []string) []string { + lhsSet := sets.New(lhs...) + rhsSet := sets.New(rhs...) + return lhsSet.Difference(rhsSet).UnsortedList() + } + + featureGatesNoExemptionsEnabled := extractFeatureGatesWithoutExemptions(fgCfg.CustomNoUpgrade.Enabled, fgCfg.SpecialHandlingSupportExceptionRequired.Enabled) + featureGatesNoExemptionsDisabled := extractFeatureGatesWithoutExemptions(fgCfg.CustomNoUpgrade.Disabled, fgCfg.SpecialHandlingSupportExceptionRequired.Disabled) return fmt.Errorf("version upgrade detected with custom feature gates: locked version %s, current version %s\n\n"+ "Upgrades are not supported when custom feature gates are configured.\n"+ @@ -154,9 +155,9 @@ func upgradeChecksPass(lockFile featureGateLockFile, fgCfg *config.FeatureGates) "2. Run: sudo microshift-cleanup-data --all\n"+ "3. Remove custom feature gates from /etc/microshift/config.yaml\n"+ "4. Restart MicroShift: sudo systemctl restart microshift", - lockFile.Version.String(), currentExecutableVersion.String(), - lockFile.Version.String(), featureGatesNoExemptionsEnabled.UnsortedList(), - featureGatesNoExemptionsDisabled.UnsortedList(), lockFile.Version.String()) + lockedVersion.String(), currentExecutableVersion.String(), + lockedVersion.String(), featureGatesNoExemptionsEnabled, + featureGatesNoExemptionsDisabled, lockedVersion.String()) } return nil } diff --git a/pkg/config/apiserver.go b/pkg/config/apiserver.go index 1ce3b2dbd7..f964b96fc1 100644 --- a/pkg/config/apiserver.go +++ b/pkg/config/apiserver.go @@ -170,8 +170,6 @@ func (fg FeatureGates) ToApiserverArgs() ([]string, error) { addFeatures(fg.CustomNoUpgrade.Enabled, true) addFeatures(fg.CustomNoUpgrade.Disabled, false) - addFeatures(fg.SpecialHandlingSupportExceptionRequired.Enabled, true) - addFeatures(fg.SpecialHandlingSupportExceptionRequired.Disabled, false) return ret.List(), nil } From 10a7752feef2a0582ed745d3bcce838c531174a4 Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Thu, 5 Feb 2026 09:08:06 -0600 Subject: [PATCH 04/10] update etcd vendor --- .../microshift/pkg/config/apiserver.go | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go b/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go index 4ed96c2166..f964b96fc1 100644 --- a/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go +++ b/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go @@ -139,7 +139,7 @@ const ( FeatureSetDevPreviewNoUpgrade = "DevPreviewNoUpgrade" ) -type CustomNoUpgrade struct { +type EnableDisableFeatures struct { Enabled []string `json:"enabled"` Disabled []string `json:"disabled"` } @@ -149,20 +149,27 @@ type CustomNoUpgrade struct { var RequiredFeatureGates = []string{"UserNamespacesSupport", "UserNamespacesPodSecurityStandards"} type FeatureGates struct { - FeatureSet string `json:"featureSet"` - CustomNoUpgrade CustomNoUpgrade `json:"customNoUpgrade"` + FeatureSet string `json:"featureSet"` + // CustomNoUpgrade is used to enable/disable feature gates. When the enabled or disable lists are not empty, x- and y-stream upgrades will be blocked. + // Use this field exclusively for custom feature gates, unless you are certain that the feature gate is a SpecialHandlingSupportExceptionRequired feature. + CustomNoUpgrade EnableDisableFeatures `json:"customNoUpgrade"` + // SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades. + // A SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features. + SpecialHandlingSupportExceptionRequired EnableDisableFeatures `json:"specialHandlingSupportExceptionRequired"` } // ToApiserverArgs converts the FeatureGates struct to a list of feature-gates arguments for the kube-apiserver. // Validation checks should be performed before calling this function to ensure the FeatureGates struct is valid. func (fg FeatureGates) ToApiserverArgs() ([]string, error) { ret := sets.NewString() - for _, feature := range fg.CustomNoUpgrade.Enabled { - ret.Insert(fmt.Sprintf("%s=true", feature)) - } - for _, feature := range fg.CustomNoUpgrade.Disabled { - ret.Insert(fmt.Sprintf("%s=false", feature)) + addFeatures := func(features []string, enabled bool) { + for _, feature := range features { + ret.Insert(fmt.Sprintf("%s=%t", feature, enabled)) + } } + + addFeatures(fg.CustomNoUpgrade.Enabled, true) + addFeatures(fg.CustomNoUpgrade.Disabled, false) return ret.List(), nil } @@ -171,49 +178,53 @@ func (fg FeatureGates) GoString() string { return fmt.Sprintf("FeatureGates{FeatureSet: %q, CustomNoUpgrade: %#v}", fg.FeatureSet, fg.CustomNoUpgrade) } +// validateFeatureGates validates the FeatureGates struct according to the following rules: +// 1. FeatureGates may be unset. +// 2. FeatureSet must be empty or CustomNoUpgrade. +// 3. If FeatureSet is DevPreviewNoUpgrade or TechPreviewNoUpgrade, return an error. +// 4. If FeatureSet is CustomNoUpgrade, CustomNoUpgrade.Enabled/Disabled lists may be set but are not required. +// 5. Required feature gates cannot be disabled. +// 6. Feature gates cannot be both enabled and disabled within the same object. func (fg *FeatureGates) validateFeatureGates() error { - // FG is unset if fg == nil || reflect.DeepEqual(*fg, FeatureGates{}) { return nil } - // FeatureSet must be empty or CustomNoUpgrade. If empty, CustomNoUpgrade.Enabled/Disabled lists must be empty. switch fg.FeatureSet { case "": - if len(fg.CustomNoUpgrade.Enabled) > 0 || len(fg.CustomNoUpgrade.Disabled) > 0 { - return fmt.Errorf("CustomNoUpgrade enabled/disabled lists must be empty when FeatureSet is empty") - } return nil case FeatureSetCustomNoUpgrade: - // Valid - continue to validate enabled/disabled lists below + // Valid - continue with validation case FeatureSetDevPreviewNoUpgrade, FeatureSetTechPreviewNoUpgrade: return fmt.Errorf("FeatureSet %s is not supported. Use CustomNoUpgrade to enable/disable feature gates", fg.FeatureSet) default: return fmt.Errorf("invalid feature set: %s", fg.FeatureSet) } - var errs = make(sets.Set[error], 0) - for _, requiredFG := range RequiredFeatureGates { - // Edge case: Users must not be allowed to explicitly disable required feature gates. - if sets.NewString(fg.CustomNoUpgrade.Disabled...).Has(requiredFG) { - errs.Insert(fmt.Errorf("required feature gate %s cannot be disabled: %s", requiredFG, fg.CustomNoUpgrade.Disabled)) - } - // Edge case: Users must not be allowed to explicitly enable required feature gates or else the config would be locked and the cluster - // would not be able to be upgraded. - if sets.New(fg.CustomNoUpgrade.Enabled...).Has(requiredFG) { - errs.Insert(fmt.Errorf("feature gate %s is explicitly enabled and cannot be enabled by the user", requiredFG)) + enabledCustom := sets.New(fg.CustomNoUpgrade.Enabled...) + disabledCustom := sets.New(fg.CustomNoUpgrade.Disabled...) + + // checkFeatureGateConflict checks if two sets of feature gates have any intersection and returns an error if they do. + checkFeatureGateConflict := func(a, b sets.Set[string], errorMsg string) error { + if intersect := a.Intersection(b); intersect.Len() > 0 { + return fmt.Errorf("%s: %s", errorMsg, intersect.UnsortedList()) } + return nil } - if errs.Len() > 0 { - return fmt.Errorf("invalid feature gates: %s", errs.UnsortedList()) + + conflictChecks := []struct { + setA sets.Set[string] + setB sets.Set[string] + msg string + }{ + {disabledCustom, sets.New(RequiredFeatureGates...), "required feature gates cannot be disabled"}, + {enabledCustom, disabledCustom, "feature gates cannot be both enabled and disabled"}, } - // Must not have any feature gates that are enabled and disabled at the same time - enabledSet := sets.New(fg.CustomNoUpgrade.Enabled...) - disabledSet := sets.New(fg.CustomNoUpgrade.Disabled...) - inBothSets := enabledSet.Intersection(disabledSet) - if inBothSets.Len() > 0 { - return fmt.Errorf("featuregates cannot be enabled and disabled at the same time: %s", inBothSets.UnsortedList()) + for _, check := range conflictChecks { + if err := checkFeatureGateConflict(check.setA, check.setB, check.msg); err != nil { + return err + } } return nil From d0bf940969098eaa02bcadc4c6991889449bef64 Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Thu, 5 Feb 2026 16:01:56 -0600 Subject: [PATCH 05/10] blocking logic fix --- pkg/admin/prerun/featuregate_lock.go | 36 +++++++++++++++------------- pkg/config/apiserver.go | 3 +-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/pkg/admin/prerun/featuregate_lock.go b/pkg/admin/prerun/featuregate_lock.go index b48c522d55..096b8ada3e 100644 --- a/pkg/admin/prerun/featuregate_lock.go +++ b/pkg/admin/prerun/featuregate_lock.go @@ -142,22 +142,26 @@ func upgradeChecksPass(lockFile featureGateLockFile, fgCfg *config.FeatureGates) return lhsSet.Difference(rhsSet).UnsortedList() } - featureGatesNoExemptionsEnabled := extractFeatureGatesWithoutExemptions(fgCfg.CustomNoUpgrade.Enabled, fgCfg.SpecialHandlingSupportExceptionRequired.Enabled) - featureGatesNoExemptionsDisabled := extractFeatureGatesWithoutExemptions(fgCfg.CustomNoUpgrade.Disabled, fgCfg.SpecialHandlingSupportExceptionRequired.Disabled) - - return fmt.Errorf("version upgrade detected with custom feature gates: locked version %s, current version %s\n\n"+ - "Upgrades are not supported when custom feature gates are configured.\n"+ - "Custom feature gates were configured in version %s.\n"+ - "Gates Enabled: %s\n"+ - "Gates Disabled: %s\n"+ - "To restore MicroShift to a supported state, you must:\n"+ - "1. Roll back to version %s, OR\n"+ - "2. Run: sudo microshift-cleanup-data --all\n"+ - "3. Remove custom feature gates from /etc/microshift/config.yaml\n"+ - "4. Restart MicroShift: sudo systemctl restart microshift", - lockedVersion.String(), currentExecutableVersion.String(), - lockedVersion.String(), featureGatesNoExemptionsEnabled, - featureGatesNoExemptionsDisabled, lockedVersion.String()) + // Extract feature gates that lack a special handling support exception. + customNoUpgradeEnabled := extractFeatureGatesWithoutExemptions(fgCfg.CustomNoUpgrade.Enabled, fgCfg.SpecialHandlingSupportExceptionRequired.Enabled) + customNoUpgradeDisabled := extractFeatureGatesWithoutExemptions(fgCfg.CustomNoUpgrade.Disabled, fgCfg.SpecialHandlingSupportExceptionRequired.Disabled) + + // If there are any gates that lack a special handling support exception, return an error. + if len(customNoUpgradeEnabled) > 0 || len(customNoUpgradeDisabled) > 0 { + return fmt.Errorf("version upgrade detected with custom feature gates: locked version %s, current version %s\n\n"+ + "Upgrades are not supported when custom feature gates are configured.\n"+ + "Custom feature gates were configured in version %s.\n"+ + "Gates Enabled: %s\n"+ + "Gates Disabled: %s\n"+ + "To restore MicroShift to a supported state, you must:\n"+ + "1. Roll back to version %s, OR\n"+ + "2. Run: sudo microshift-cleanup-data --all\n"+ + "3. Remove custom feature gates from /etc/microshift/config.yaml\n"+ + "4. Restart MicroShift: sudo systemctl restart microshift", + lockedVersion.String(), currentExecutableVersion.String(), + lockedVersion.String(), customNoUpgradeEnabled, + customNoUpgradeDisabled, lockedVersion.String()) + } } return nil } diff --git a/pkg/config/apiserver.go b/pkg/config/apiserver.go index f964b96fc1..54f6b29129 100644 --- a/pkg/config/apiserver.go +++ b/pkg/config/apiserver.go @@ -153,8 +153,7 @@ type FeatureGates struct { // CustomNoUpgrade is used to enable/disable feature gates. When the enabled or disable lists are not empty, x- and y-stream upgrades will be blocked. // Use this field exclusively for custom feature gates, unless you are certain that the feature gate is a SpecialHandlingSupportExceptionRequired feature. CustomNoUpgrade EnableDisableFeatures `json:"customNoUpgrade"` - // SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades. - // A SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features. + // SpecialHandlingSupportExceptionRequired allows for feature gates to be exempted from blocking x- and y-stream upgrades. SpecialHandlingSupportExceptionRequired EnableDisableFeatures `json:"specialHandlingSupportExceptionRequired"` } From 546eacebfd285fb243b9e1416986f54fc02b2512 Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Fri, 6 Feb 2026 11:55:12 -0600 Subject: [PATCH 06/10] regenerate config --- cmd/generate-config/config/config-openapi-spec.json | 2 +- packaging/microshift/config.yaml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/generate-config/config/config-openapi-spec.json b/cmd/generate-config/config/config-openapi-spec.json index c4deb12600..fe3fb34c77 100755 --- a/cmd/generate-config/config/config-openapi-spec.json +++ b/cmd/generate-config/config/config-openapi-spec.json @@ -94,7 +94,7 @@ "type": "string" }, "specialHandlingSupportExceptionRequired": { - "description": "SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades.\nA SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features.", + "description": "SpecialHandlingSupportExceptionRequired allows for feature gates to be exempted from blocking x- and y-stream upgrades.", "type": "object", "required": [ "disabled", diff --git a/packaging/microshift/config.yaml b/packaging/microshift/config.yaml index 5a6147e034..bcddbe6db1 100644 --- a/packaging/microshift/config.yaml +++ b/packaging/microshift/config.yaml @@ -21,8 +21,7 @@ apiServer: disabled: [] enabled: [] featureSet: "" - # SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades. - # A SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features. + # SpecialHandlingSupportExceptionRequired allows for feature gates to be exempted from blocking x- and y-stream upgrades. specialHandlingSupportExceptionRequired: disabled: [] enabled: [] From 726499df217deaea9b327b3c6d5f1c51da93301d Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Fri, 6 Feb 2026 11:56:18 -0600 Subject: [PATCH 07/10] update etcd vendor --- .../github.com/openshift/microshift/pkg/config/apiserver.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go b/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go index f964b96fc1..54f6b29129 100644 --- a/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go +++ b/etcd/vendor/github.com/openshift/microshift/pkg/config/apiserver.go @@ -153,8 +153,7 @@ type FeatureGates struct { // CustomNoUpgrade is used to enable/disable feature gates. When the enabled or disable lists are not empty, x- and y-stream upgrades will be blocked. // Use this field exclusively for custom feature gates, unless you are certain that the feature gate is a SpecialHandlingSupportExceptionRequired feature. CustomNoUpgrade EnableDisableFeatures `json:"customNoUpgrade"` - // SpecialHandlingSupportExceptionRequired is used to enable/disable feature gates without blocking x- and y-stream upgrades. - // A SpecialHandlingSupportExceptionRequired feature will be given precedence over the same feature (if set) in CustomNoUpgrade features. + // SpecialHandlingSupportExceptionRequired allows for feature gates to be exempted from blocking x- and y-stream upgrades. SpecialHandlingSupportExceptionRequired EnableDisableFeatures `json:"specialHandlingSupportExceptionRequired"` } From 166b15b4ef3d5a91ddc273543d3061122237215a Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Fri, 6 Feb 2026 12:52:54 -0600 Subject: [PATCH 08/10] add unit tests for special handling upgrade exceptions --- pkg/admin/prerun/featuregate_lock_test.go | 89 ++++++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/pkg/admin/prerun/featuregate_lock_test.go b/pkg/admin/prerun/featuregate_lock_test.go index 12dfc9673e..24fc2c3576 100644 --- a/pkg/admin/prerun/featuregate_lock_test.go +++ b/pkg/admin/prerun/featuregate_lock_test.go @@ -365,18 +365,24 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { } tests := []struct { - name string - lockFileVer versionMetadata - currentVer versionMetadata - wantErr bool - description string + name string + lockFileVer versionMetadata + currentVer versionMetadata + customNoUpgrade *config.EnableDisableFeatures + specialHandlingSupportException *config.EnableDisableFeatures + wantErr bool + description string }{ { - name: "minor version upgrade should fail", - lockFileVer: getVersion(0, 0, 0), - currentVer: getVersion(0, 1, 0), - wantErr: true, - description: "Minor version upgrade (4.21.0 -> 4.22.0) should be blocked", + name: "minor version upgrade should fail", + lockFileVer: getVersion(0, 0, 0), + currentVer: getVersion(0, 1, 0), + wantErr: true, + specialHandlingSupportException: &config.EnableDisableFeatures{}, + description: "Minor version upgrade (4.21.0 -> 4.22.0) should be blocked", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, }, { name: "major version upgrade should fail", @@ -384,6 +390,10 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { currentVer: getVersion(1, 0, 0), wantErr: true, description: "Major version upgrade (4.21.0 -> 5.0.0) should be blocked", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{}, }, { name: "patch version change should succeed", @@ -391,6 +401,10 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { currentVer: getVersion(0, 0, 1), wantErr: false, description: "Patch version change (4.21.0 -> 4.21.1) should be allowed", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{}, }, { name: "same version should succeed", @@ -398,6 +412,10 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { currentVer: getVersion(0, 0, 0), wantErr: false, description: "Same version (4.21.0 -> 4.21.0) should be allowed", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{}, }, { name: "minor version downgrade should fail", @@ -405,6 +423,10 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { currentVer: getVersion(0, 0, 0), wantErr: true, description: "Minor version downgrade (4.22.0 -> 4.21.0) should be blocked", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{}, }, { name: "major version downgrade should fail", @@ -412,6 +434,36 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { currentVer: getVersion(0, 0, 0), wantErr: true, description: "Major version downgrade (5.0.0 -> 4.21.0) should be blocked", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{}, + }, + { + name: "major version upgrade with special handling support exception should succeed", + lockFileVer: getVersion(0, 0, 0), + currentVer: getVersion(1, -21, 0), + wantErr: false, + description: "major version upgrade (4.21.0 -> 5.0.0) with special handling support exception should succeed", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + }, + { + name: "minor version upgrade with special handling support exception should succeed", + lockFileVer: getVersion(0, 0, 0), + currentVer: getVersion(1, -21, 0), + wantErr: false, + description: "minor version upgrade (4.21.0 -> 4.22.0) with special handling support exception should succeed", + customNoUpgrade: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, + specialHandlingSupportException: &config.EnableDisableFeatures{ + Enabled: []string{"FeatureA"}, + }, }, } @@ -436,13 +488,11 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { } defer func() { getExecutableVersion = originalGetExecutableVersion }() - // Create lockFile file with locked version + // Create lockFile file with locked version. Lock file does not store the special handling support exception. lockFile := featureGateLockFile{ - FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.EnableDisableFeatures{ - Enabled: []string{"FeatureA"}, - }, - Version: tt.lockFileVer, + FeatureSet: config.FeatureSetCustomNoUpgrade, + CustomNoUpgrade: *tt.customNoUpgrade, + Version: tt.lockFileVer, } if err := writeFeatureGateLockFile(featureGateLockFilePath, lockFile); err != nil { t.Fatal(err) @@ -451,10 +501,9 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { cfg := &config.Config{ ApiServer: config.ApiServer{ FeatureGates: config.FeatureGates{ - FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: config.EnableDisableFeatures{ - Enabled: []string{"FeatureA"}, - }, + FeatureSet: config.FeatureSetCustomNoUpgrade, + CustomNoUpgrade: *tt.customNoUpgrade, + SpecialHandlingSupportExceptionRequired: *tt.specialHandlingSupportException, }, }, } From 50c0610ac5b7adb7c27505e60096a7cbb5baeaaa Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Mon, 16 Feb 2026 14:49:36 -0600 Subject: [PATCH 09/10] fixed unit test values --- pkg/admin/prerun/featuregate_lock_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/admin/prerun/featuregate_lock_test.go b/pkg/admin/prerun/featuregate_lock_test.go index 24fc2c3576..f6f8cc6d0e 100644 --- a/pkg/admin/prerun/featuregate_lock_test.go +++ b/pkg/admin/prerun/featuregate_lock_test.go @@ -455,7 +455,7 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { { name: "minor version upgrade with special handling support exception should succeed", lockFileVer: getVersion(0, 0, 0), - currentVer: getVersion(1, -21, 0), + currentVer: getVersion(0, 1, 0), wantErr: false, description: "minor version upgrade (4.21.0 -> 4.22.0) with special handling support exception should succeed", customNoUpgrade: &config.EnableDisableFeatures{ From 68e7e71d514dfb88bc83cd9ab22c0b872d787252 Mon Sep 17 00:00:00 2001 From: Jon Cope Date: Tue, 17 Feb 2026 10:01:48 -0600 Subject: [PATCH 10/10] fixed expected output in unit test --- pkg/admin/prerun/featuregate_lock_test.go | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/admin/prerun/featuregate_lock_test.go b/pkg/admin/prerun/featuregate_lock_test.go index f6f8cc6d0e..a8c1ac9c81 100644 --- a/pkg/admin/prerun/featuregate_lock_test.go +++ b/pkg/admin/prerun/featuregate_lock_test.go @@ -110,7 +110,7 @@ func TestIsCustomFeatureGatesConfigured(t *testing.T) { fg: config.FeatureGates{ FeatureSet: "", }, - want: false, + want: true, // validation passes when prev and current both have no feature set }, { name: "CustomNoUpgrade without any enabled/disabled", @@ -118,7 +118,7 @@ func TestIsCustomFeatureGatesConfigured(t *testing.T) { FeatureSet: config.FeatureSetCustomNoUpgrade, CustomNoUpgrade: config.EnableDisableFeatures{}, }, - want: false, + want: true, // validation passes when prev and current match }, } @@ -128,11 +128,9 @@ func TestIsCustomFeatureGatesConfigured(t *testing.T) { FeatureSet: tt.fg.FeatureSet, CustomNoUpgrade: tt.fg.CustomNoUpgrade, }, &tt.fg) - if err != nil { - t.Errorf("featureValidationsPass() error = %v", err) - } - if err != nil { - t.Errorf("featureValidationsPass() error = %v", err) + got := err == nil + if got != tt.want { + t.Errorf("configValidationChecksPass() got pass = %v, want %v (err = %v)", got, tt.want, err) } }) } @@ -489,9 +487,17 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { defer func() { getExecutableVersion = originalGetExecutableVersion }() // Create lockFile file with locked version. Lock file does not store the special handling support exception. + customNoUpgrade := config.EnableDisableFeatures{} + if tt.customNoUpgrade != nil { + customNoUpgrade = *tt.customNoUpgrade + } + specialHandling := config.EnableDisableFeatures{} + if tt.specialHandlingSupportException != nil { + specialHandling = *tt.specialHandlingSupportException + } lockFile := featureGateLockFile{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: *tt.customNoUpgrade, + CustomNoUpgrade: customNoUpgrade, Version: tt.lockFileVer, } if err := writeFeatureGateLockFile(featureGateLockFilePath, lockFile); err != nil { @@ -502,8 +508,8 @@ func TestFeatureGateLockManagement_VersionChange(t *testing.T) { ApiServer: config.ApiServer{ FeatureGates: config.FeatureGates{ FeatureSet: config.FeatureSetCustomNoUpgrade, - CustomNoUpgrade: *tt.customNoUpgrade, - SpecialHandlingSupportExceptionRequired: *tt.specialHandlingSupportException, + CustomNoUpgrade: customNoUpgrade, + SpecialHandlingSupportExceptionRequired: specialHandling, }, }, }