diff --git a/go.mod b/go.mod index 23fd05f43..0b4281a87 100644 --- a/go.mod +++ b/go.mod @@ -133,3 +133,5 @@ require ( ) replace github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1 + +replace github.com/openshift/library-go => github.com/ardaguclu/library-go v0.0.0-20260618055608-feb27829a74c diff --git a/go.sum b/go.sum index f741edfe8..65c412997 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2 h1:x8Brv0YNEe6 github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2/go.mod h1:XyjUkMA8GN+tOOPXvnbi3XuRxWFvTJntqvTFnjmhzbk= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/ardaguclu/library-go v0.0.0-20260618055608-feb27829a74c h1:wCyA9uDne5jqE8Lv7p6Gk2tHyTfpJ+7weCCnHPZcr4I= +github.com/ardaguclu/library-go v0.0.0-20260618055608-feb27829a74c/go.mod h1:/HBhy6jm/igWI3Y1vYFwFG3ZCcXmnNsKUT6VBpPyM9A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -152,8 +154,6 @@ github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af h1:Ui github.com/openshift/build-machinery-go v0.0.0-20251023084048-5d77c1a5e5af/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20260512113608-deb4dc54551a h1:EKx2XhOKehd1C5ptY7IrLl4WV35E8kP0pRPnG5BUZXk= github.com/openshift/client-go v0.0.0-20260512113608-deb4dc54551a/go.mod h1:V933kvY/cb/Un7UCEOhXHUySNX327u7Epe8g9KNqg2Q= -github.com/openshift/library-go v0.0.0-20260615113748-bc9d4056464b h1:fmTTzoHLhRf4QsMKw4cEbnvUeUcgcaNesh5mueWnaVs= -github.com/openshift/library-go v0.0.0-20260615113748-bc9d4056464b/go.mod h1:/HBhy6jm/igWI3Y1vYFwFG3ZCcXmnNsKUT6VBpPyM9A= github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d h1:Rzx23P63JFNNz5D23ubhC0FCN5rK8CeJhKcq5QKcdyU= github.com/openshift/multi-operator-manager v0.0.0-20241205181422-20aa3906b99d/go.mod h1:iVi9Bopa5cLhjG5ie9DoZVVqkH8BGb1FQVTtecOLn4I= github.com/openshift/oauth-apiserver v0.0.0-20260430140618-160ac7fb4ea6 h1:WvXToDt/IVTXb4NxbqEjY0cuPpVadTK6ATu75mlVM/s= diff --git a/test/e2e-encryption-kms/encryption_kms.go b/test/e2e-encryption-kms/encryption_kms.go index d1de50db4..771ad04e0 100644 --- a/test/e2e-encryption-kms/encryption_kms.go +++ b/test/e2e-encryption-kms/encryption_kms.go @@ -14,12 +14,20 @@ import ( ) var _ = g.Describe("[sig-auth] cluster-authentication-operator", func() { - g.It("TestKMSEncryptionOnOff [OCPFeatureGate:KMSEncryption][Serial][Timeout:120m]", func(ctx context.Context) { - testKMSEncryptionOnOff(ctx, g.GinkgoTB()) + //g.It("TestKMSEncryptionOnOff [OCPFeatureGate:KMSEncryption][Serial][Timeout:120m]", func(ctx context.Context) { + // testKMSEncryptionOnOff(ctx, g.GinkgoTB()) + //}) + + //g.It("TestKMSEncryptionProvidersMigration [OCPFeatureGate:KMSEncryption][Serial][Timeout:120m]", func(ctx context.Context) { + // testKMSEncryptionProvidersMigration(ctx, g.GinkgoTB()) + //}) + + g.It("TestKMSToKMSMigration [OCPFeatureGate:KMSEncryption][Serial][Timeout:120m]", func(ctx context.Context) { + testKMSToKMSMigration(ctx, g.GinkgoTB()) }) - g.It("TestKMSEncryptionProvidersMigration [OCPFeatureGate:KMSEncryption][Serial][Timeout:120m]", func(ctx context.Context) { - testKMSEncryptionProvidersMigration(ctx, g.GinkgoTB()) + g.It("TestKMSToKMSOnOff [OCPFeatureGate:KMSEncryption][Serial][Timeout:120m]", func(ctx context.Context) { + testKMSToKMSOnOff(ctx, g.GinkgoTB()) }) }) @@ -88,3 +96,51 @@ func testKMSEncryptionProvidersMigration(ctx context.Context, t testing.TB) { }), }) } + +// testKMSToKMSMigration tests KMS-to-KMS migration (primary → secondary → primary → identity). +func testKMSToKMSMigration(ctx context.Context, t testing.TB) { + library.TestKMSToKMSMigration(ctx, t, library.KMSToKMSMigrationScenario{ + BasicScenario: library.BasicScenario{ + Namespace: "openshift-config-managed", + LabelSelector: "encryption.apiserver.operator.openshift.io/component" + "=" + "openshift-oauth-apiserver", + EncryptionConfigSecretName: "encryption-config-openshift-oauth-apiserver", + EncryptionConfigSecretNamespace: "openshift-config-managed", + OperatorNamespace: "openshift-authentication-operator", + TargetGRs: operatorencryption.DefaultTargetGRs, + AssertFunc: operatorencryption.AssertTokens, + }, + CreateResourceFunc: func(t testing.TB, _ library.ClientSet, namespace string) runtime.Object { + return operatorencryption.CreateAndStoreTokenOfLife(context.TODO(), t, operatorencryption.GetClients(t)) + }, + AssertResourceEncryptedFunc: operatorencryption.AssertTokenOfLifeEncrypted, + AssertResourceNotEncryptedFunc: operatorencryption.AssertTokenOfLifeNotEncrypted, + ResourceFunc: func(t testing.TB, _ string) runtime.Object { return operatorencryption.TokenOfLife(t) }, + ResourceName: "TokenOfLife", + PrimaryProvider: librarykms.DefaultVaultEncryptionProvider(ctx, t), + SecondaryProvider: librarykms.SecondaryVaultEncryptionProvider(ctx, t), + }) +} + +// testKMSToKMSOnOff tests KMS on/off cycle with two distinct KMS providers. +func testKMSToKMSOnOff(ctx context.Context, t testing.TB) { + library.TestKMSToKMSOnOff(ctx, t, library.KMSToKMSMigrationScenario{ + BasicScenario: library.BasicScenario{ + Namespace: "openshift-config-managed", + LabelSelector: "encryption.apiserver.operator.openshift.io/component" + "=" + "openshift-oauth-apiserver", + EncryptionConfigSecretName: "encryption-config-openshift-oauth-apiserver", + EncryptionConfigSecretNamespace: "openshift-config-managed", + OperatorNamespace: "openshift-authentication-operator", + TargetGRs: operatorencryption.DefaultTargetGRs, + AssertFunc: operatorencryption.AssertTokens, + }, + CreateResourceFunc: func(t testing.TB, _ library.ClientSet, namespace string) runtime.Object { + return operatorencryption.CreateAndStoreTokenOfLife(context.TODO(), t, operatorencryption.GetClients(t)) + }, + AssertResourceEncryptedFunc: operatorencryption.AssertTokenOfLifeEncrypted, + AssertResourceNotEncryptedFunc: operatorencryption.AssertTokenOfLifeNotEncrypted, + ResourceFunc: func(t testing.TB, _ string) runtime.Object { return operatorencryption.TokenOfLife(t) }, + ResourceName: "TokenOfLife", + PrimaryProvider: librarykms.DefaultVaultEncryptionProvider(ctx, t), + SecondaryProvider: librarykms.SecondaryVaultEncryptionProvider(ctx, t), + }) +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go index ac6677a9c..ccee4298b 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/key_controller.go @@ -199,9 +199,18 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact // fills up the state with all resources and set identity write key if write key secrets // are missing. + var providerCfg kmsProviderConfig = noopKMSProviderConfig{} + if currentMode == state.KMS { + var err error + providerCfg, err = newKMSProviderConfig(apiEncryptionConfiguration.KMS) + if err != nil { + return err + } + } + var commonReason *string for gr, grKeys := range desiredEncryptionState { - latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs) + latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs, providerCfg) if !needed { continue } @@ -228,7 +237,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact sort.Sort(sort.StringSlice(reasons)) internalReason := strings.Join(reasons, ", ") - keySecret, err := c.generateKeySecret(ctx, newKeyID, currentMode, apiEncryptionConfiguration, internalReason, externalReason) + keySecret, err := c.generateKeySecret(ctx, newKeyID, currentMode, apiEncryptionConfiguration, providerCfg, internalReason, externalReason) if err != nil { return fmt.Errorf("failed to create key: %v", err) } @@ -265,7 +274,7 @@ func (c *keyController) validateExistingSecret(ctx context.Context, keySecret *c return nil // we made this key earlier } -func (c *keyController) generateKeySecret(ctx context.Context, keyID uint64, currentMode state.Mode, apiServerEncryption configv1.APIServerEncryption, internalReason, externalReason string) (*corev1.Secret, error) { +func (c *keyController) generateKeySecret(ctx context.Context, keyID uint64, currentMode state.Mode, apiServerEncryption configv1.APIServerEncryption, providerCfg kmsProviderConfig, internalReason, externalReason string) (*corev1.Secret, error) { bs := crypto.ModeToNewKeyFunc[currentMode]() ks := state.KeyState{ Key: apiserverv1.Key{ @@ -287,11 +296,6 @@ func (c *keyController) generateKeySecret(ctx context.Context, keyID uint64, cur Plugin: apiServerEncryption.KMS, } - providerCfg, err := newKMSProviderConfig(apiServerEncryption.KMS) - if err != nil { - return nil, err - } - if secretName, expectedKeys, err := providerCfg.referencedSecretName(); err != nil { return nil, err } else if len(secretName) > 0 { @@ -363,7 +367,7 @@ func (c *keyController) getCurrentModeReasonAndEncryptionConfig(ctx context.Cont // needsNewKey checks whether a new key must be created for the given resource. If true, it also returns the latest // used key ID and a reason string. -func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, externalReason string, encryptedGRs []schema.GroupResource) (uint64, string, bool) { +func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, externalReason string, encryptedGRs []schema.GroupResource, providerCfg kmsProviderConfig) (uint64, string, bool) { // we always need to have some encryption keys unless we are turned off if len(grKeys.ReadKeys) == 0 { return 0, "key-does-not-exist", currentMode != state.Identity @@ -408,13 +412,20 @@ func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, extern if currentMode == state.KMS { // We are here because Encryption Mode is not changed + // However, we need to create a new key if migration-triggering fields + // in the KMS provider configuration have changed. + if latestKey.KMS == nil { + // A KMS-mode key without KMS state indicates a corrupted key secret. + // Do not create a new key on corrupted data. + klog.Warningf("KMS-mode key %q has nil KMS state, possibly corrupted key secret; skipping new key creation", latestKey.Key.Name) + return 0, "", false + } + if providerCfg.migrationRequired(latestKey.KMS.Plugin) { + return latestKeyID, "kms-provider-changed", true + } - // For now in Tech Preview v1, we don't support configurational changes. Therefore, - // it is pointless comparing the secrets. - - // For KMS mode, we don't do time-based rotation. Therefore, we shortcut here - // KMS keys are rotated externally by the KMS system. - // Moreover, we don't trigger new key when external reason is changed. + // For KMS mode, we don't do time-based rotation. KMS keys are rotated + // externally by the KMS provider. Moreover, we don't trigger new key when external reason is changed. // Because it would lead to duplicate providers which is not allowed. return 0, "", false } @@ -440,8 +451,19 @@ type kmsProviderConfig interface { // config and the specific data keys to carry from that configmap. Only the listed keys // are copied into the Key Secret; any other data in the referenced configmap is ignored. referencedConfigMapName() (string, []string, error) + // migrationRequired reports whether switching from latest (stored in + // the key secret) to this provider config requires a new encryption key. + migrationRequired(latest configv1.KMSPluginConfig) bool } +// noopKMSProviderConfig is a safe zero-value implementation used for non-KMS modes. +// All methods return empty/false so callers never need nil checks. +type noopKMSProviderConfig struct{} + +func (noopKMSProviderConfig) referencedSecretName() (string, []string, error) { return "", nil, nil } +func (noopKMSProviderConfig) referencedConfigMapName() (string, []string, error) { return "", nil, nil } +func (noopKMSProviderConfig) migrationRequired(configv1.KMSPluginConfig) bool { return false } + func newKMSProviderConfig(plugin configv1.KMSPluginConfig) (kmsProviderConfig, error) { switch plugin.Type { case configv1.VaultKMSProvider: @@ -473,6 +495,30 @@ func (v *vaultProviderConfig) referencedConfigMapName() (string, []string, error return v.vault.TLS.CABundle.Name, []string{"ca-bundle.crt"}, nil } +func (v *vaultProviderConfig) migrationRequired(latest configv1.KMSPluginConfig) bool { + if latest.Type != configv1.VaultKMSProvider { + klog.V(2).Infof("KMS migration required: provider type changed from %q to %q", latest.Type, configv1.VaultKMSProvider) + return true + } + if v.vault.VaultAddress != latest.Vault.VaultAddress { + klog.V(2).Infof("KMS migration required: VaultAddress changed from %q to %q", latest.Vault.VaultAddress, v.vault.VaultAddress) + return true + } + if v.vault.VaultNamespace != latest.Vault.VaultNamespace { + klog.V(2).Infof("KMS migration required: VaultNamespace changed from %q to %q", latest.Vault.VaultNamespace, v.vault.VaultNamespace) + return true + } + if v.vault.TransitMount != latest.Vault.TransitMount { + klog.V(2).Infof("KMS migration required: TransitMount changed from %q to %q", latest.Vault.TransitMount, v.vault.TransitMount) + return true + } + if v.vault.TransitKey != latest.Vault.TransitKey { + klog.V(2).Infof("KMS migration required: TransitKey changed from %q to %q", latest.Vault.TransitKey, v.vault.TransitKey) + return true + } + return false +} + // TODO make this un-settable once set // ex: we could require the tech preview no upgrade flag to be set before we will honor this field type unsupportedEncryptionConfig struct { diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/kms_preflight_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/kms_preflight_controller.go new file mode 100644 index 000000000..4a7ea5577 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/controllers/kms_preflight_controller.go @@ -0,0 +1,166 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + operatorv1 "github.com/openshift/api/operator/v1" + configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1" + configv1informers "github.com/openshift/client-go/config/informers/externalversions/config/v1" + applyoperatorv1 "github.com/openshift/client-go/operator/applyconfigurations/operator/v1" + + "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/events" + operatorv1helpers "github.com/openshift/library-go/pkg/operator/v1helpers" +) + +type kmsPreflightController struct { + controllerInstanceName string + + operatorClient operatorv1helpers.OperatorClient + apiServerClient configv1client.APIServerInterface + + provider Provider + preconditionsFulfilledFn preconditionsFulfilled +} + +// NewKMSPreflightController validates KMS configuration before a key is created. +// +// Coordination with the key-controller: +// +// The key-controller writes a hash of the current KMS config to operator status +// as the EncryptionKMSPreflightRequired condition (hash in the message). +// This controller reads that hash, runs preflight checks, and on success sets +// the EncryptionKMSPreflightSucceeded condition (same hash in the message). +// The key-controller waits for the two hashes to match before creating a key. +// +// This is the same pattern used by the revision and installer controllers: +// the revision controller writes LatestAvailableRevision, the installer +// controller reads it and acts. +// +// Without this protocol the following race can occur: +// 1. Preflight passes for config A, hash A written to operator status. +// 2. Key-controller reads hash A, starts creating a key for config A. +// 3. Config changes to B. +// 4. Preflight controller syncs, sees config B, does not yet see the key +// for A (key-controller is in the process of creating the key), +// runs preflight for B, overwrites status with hash B. +// 5. The key created in step 2 was for config A but status now says B. +// +// Letting the key-controller own EncryptionKMSPreflightRequired and this +// controller own EncryptionKMSPreflightSucceeded solves this. If the config +// changes mid-flight the key-controller posts a new hash and the preflight +// controller sees the mismatch and waits. +// +// Example 1: config changes before key is created +// 1. User creates KMS config A. +// 2. Key-controller computes hash A, writes EncryptionKMSPreflightRequired=A. +// 3. Preflight controller sees required=A, starts checking A. +// 4. User changes config to A2 (minor variation, different hash). +// 5. Key-controller computes hash A2, writes EncryptionKMSPreflightRequired=A2. +// 6. Preflight controller sees required=A2, starts checking A2. +// 7. Key-controller does not create a key until succeeded=A2. +// +// Example 2: config changes after key is created +// 1. User creates KMS config A. +// 2. Key-controller computes hash A, writes EncryptionKMSPreflightRequired=A. +// 3. Preflight controller checks A, succeeds, writes EncryptionKMSPreflightSucceeded=A. +// 4. Key-controller sees required=A matches succeeded=A, creates key for A. +// 5. User changes config to A2 (or B). +// 6. Key-controller waits until the key for A completes the full cycle +// (read, write, migrated) before creating a new key. No preflight done +// at this stage. +// +// Preflight workload: +// +// A deployer interface abstracts the workload creation. Each operator provides +// its own implementation that knows how to install, get status, clean up the +// preflight workload, and wire the credentials needed to update pod status. +// The workload type matches the API server it validates (static pod for kas-o, +// Deployment for aggregated API servers). +// +// When an existing KMS plugin is already configured, the checker runs the new +// plugin alongside the existing one to catch co-existence issues (e.g., metric +// port collisions). When no plugin is configured yet, it runs the new plugin alone. +// The sync method reads existing encryption key secrets to determine whether +// a plugin is already configured. +// +// The pod uses readiness gates to post check results back to the controller. +// To set the readiness gate condition, the pod PATCHes its own status using +// credentials wired by the deployer. +// The controller reads these enhanced pod statuses to update its own operator +// status, which is propagated to end users. +// +// After a successful check the preflight pod is kept for a short period (e.g. 1h) +// so that its logs can be inspected, then cleaned up by a subsequent sync. +func NewKMSPreflightController( + instanceName string, + provider Provider, + preconditionsFulfilledFn preconditionsFulfilled, + operatorClient operatorv1helpers.OperatorClient, + apiServerClient configv1client.APIServerInterface, + apiServerInformer configv1informers.APIServerInformer, + eventRecorder events.Recorder, +) factory.Controller { + c := &kmsPreflightController{ + controllerInstanceName: factory.ControllerInstanceName(instanceName, "EncryptionKMSPreflight"), + + operatorClient: operatorClient, + apiServerClient: apiServerClient, + + provider: provider, + preconditionsFulfilledFn: preconditionsFulfilledFn, + } + + return factory.New(). + WithSync(c.sync). + WithControllerInstanceName(c.controllerInstanceName). + ResyncEvery(time.Minute). + WithInformers( + apiServerInformer.Informer(), + operatorClient.Informer(), + ).ToController( + c.controllerInstanceName, + eventRecorder.WithComponentSuffix("encryption-kms-preflight-controller"), + ) +} + +func (c *kmsPreflightController) sync(ctx context.Context, syncCtx factory.SyncContext) (err error) { + degradedCondition := applyoperatorv1.OperatorCondition().WithType("EncryptionKMSPreflightControllerDegraded") + + defer func() { + if degradedCondition == nil { + return + } + status := applyoperatorv1.OperatorStatus().WithConditions(degradedCondition) + if applyError := c.operatorClient.ApplyOperatorStatus(ctx, c.controllerInstanceName, status); applyError != nil { + err = applyError + } + }() + + if ready, err := shouldRunEncryptionController(c.operatorClient, c.preconditionsFulfilledFn, c.provider.ShouldRunEncryptionControllers); err != nil || !ready { + if err != nil { + degradedCondition = nil + } else { + degradedCondition = degradedCondition.WithStatus(operatorv1.ConditionFalse) + } + return err // we will get re-kicked when the operator status updates + } + + preflightErr := c.runPreflightChecks(ctx) + if preflightErr != nil { + degradedCondition = degradedCondition. + WithStatus(operatorv1.ConditionTrue). + WithReason("Error"). + WithMessage(preflightErr.Error()) + } else { + degradedCondition = degradedCondition. + WithStatus(operatorv1.ConditionFalse) + } + return preflightErr +} + +func (c *kmsPreflightController) runPreflightChecks(ctx context.Context) error { + return fmt.Errorf("implement me") +} diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go b/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go index aa88a9b6f..2278ecb84 100644 --- a/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go +++ b/vendor/github.com/openshift/library-go/test/library/encryption/helpers.go @@ -94,12 +94,13 @@ func SetAndWaitForEncryptionType(ctx context.Context, t testing.TB, provider Enc apiServer, err := clientSet.ApiServerConfig.Get(ctx, "cluster", metav1.GetOptions{}) require.NoError(t, err) - needsUpdate := !equality.Semantic.DeepEqual(apiServer.Spec.Encryption, provider.APIServerEncryption) + previousEncryption := apiServer.Spec.Encryption + needsUpdate := !equality.Semantic.DeepEqual(previousEncryption, provider.APIServerEncryption) if needsUpdate { if provider.Setup != nil { provider.Setup(ctx, t) } - t.Logf("Updating encryption configuration for APIServer from %#v to %#v", apiServer.Spec.Encryption, provider.APIServerEncryption) + t.Logf("Updating encryption configuration for APIServer from %#v to %#v", previousEncryption, provider.APIServerEncryption) apiServer.Spec.Encryption = provider.APIServerEncryption _, err = clientSet.ApiServerConfig.Update(ctx, apiServer, metav1.UpdateOptions{}) require.NoError(t, err) @@ -107,6 +108,14 @@ func SetAndWaitForEncryptionType(ctx context.Context, t testing.TB, provider Enc t.Logf("APIServer is already configured to use %q mode", provider.Type) } + // KMS-to-KMS migration: when both old and new are KMS but the config differs, + // the key controller creates a new key. We must wait for the next migrated key + // rather than asserting no new key is created. + if needsUpdate && provider.Type == configv1.EncryptionTypeKMS && previousEncryption.Type == configv1.EncryptionTypeKMS { + WaitForNextMigratedKey(t, clientSet.Kube, lastMigratedKeyMeta, defaultTargetGRs, namespace, labelSelector) + return clientSet + } + WaitForEncryptionKeyBasedOn(t, clientSet.Kube, lastMigratedKeyMeta, provider.Type, defaultTargetGRs, namespace, labelSelector) return clientSet } diff --git a/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go b/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go index 9885f4f97..bc923e96e 100644 --- a/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go +++ b/vendor/github.com/openshift/library-go/test/library/encryption/scenarios.go @@ -354,6 +354,100 @@ func TestKMSInvalidEncryptionRecovery(ctx context.Context, t testing.TB, scenari } } +// KMSToKMSMigrationScenario tests migration between two distinct KMS configurations +// (e.g. different Vault instances or transit keys). Unlike in-place updates, changing a +// key-triggering field (transit key, vault address, etc.) causes the operator to mint a +// new encryption key and re-encrypt all resources. +type KMSToKMSMigrationScenario struct { + BasicScenario + CreateResourceFunc func(t testing.TB, clientSet ClientSet, namespace string) runtime.Object + AssertResourceEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object) + AssertResourceNotEncryptedFunc func(t testing.TB, clientSet ClientSet, resource runtime.Object) + ResourceFunc func(t testing.TB, namespace string) runtime.Object + ResourceName string + PrimaryProvider EncryptionProvider + SecondaryProvider EncryptionProvider +} + +// TestKMSToKMSMigration validates round-trip migration between two KMS providers: +// 1. Create a test resource +// 2. Encrypt with primary KMS provider and verify +// 3. Migrate to secondary KMS provider — a new key is created and resources re-encrypted +// 4. Migrate back to primary KMS provider — verifies returning to original works +// 5. Switch to identity — verify resources are decrypted +func TestKMSToKMSMigration(ctx context.Context, t testing.TB, scenario KMSToKMSMigrationScenario) { + require.NotNil(t, scenario.PrimaryProvider.Setup, "PrimaryProvider.Setup must not be nil") + require.NotNil(t, scenario.SecondaryProvider.Setup, "SecondaryProvider.Setup must not be nil") + require.Equal(t, configv1.EncryptionTypeKMS, scenario.PrimaryProvider.Type, "PrimaryProvider must use KMS encryption type") + require.Equal(t, configv1.EncryptionTypeKMS, scenario.SecondaryProvider.Type, "SecondaryProvider must use KMS encryption type") + + TestEncryptionProvidersMigration(ctx, t, ProvidersMigrationScenario{ + BasicScenario: scenario.BasicScenario, + CreateResourceFunc: scenario.CreateResourceFunc, + AssertResourceEncryptedFunc: scenario.AssertResourceEncryptedFunc, + AssertResourceNotEncryptedFunc: scenario.AssertResourceNotEncryptedFunc, + ResourceFunc: scenario.ResourceFunc, + ResourceName: scenario.ResourceName, + EncryptionProviders: []EncryptionProvider{ + scenario.PrimaryProvider, + scenario.SecondaryProvider, + scenario.PrimaryProvider, + }, + }) +} + +// TestKMSToKMSOnOff validates KMS on/off cycle using the KMS-to-KMS scenario struct: +// 1. Create a test resource +// 2. Encrypt with primary KMS → verify encrypted +// 3. Switch to identity → verify decrypted +// 4. Encrypt with secondary KMS → verify encrypted +// 5. Switch to identity → verify decrypted +func TestKMSToKMSOnOff(ctx context.Context, t testing.TB, scenario KMSToKMSMigrationScenario) { + require.NotNil(t, scenario.PrimaryProvider.Setup, "PrimaryProvider.Setup must not be nil") + require.NotNil(t, scenario.SecondaryProvider.Setup, "SecondaryProvider.Setup must not be nil") + require.Equal(t, configv1.EncryptionTypeKMS, scenario.PrimaryProvider.Type, "PrimaryProvider must use KMS encryption type") + require.Equal(t, configv1.EncryptionTypeKMS, scenario.SecondaryProvider.Type, "SecondaryProvider must use KMS encryption type") + + e := NewE(t, PrintEventsOnFailure(scenario.OperatorNamespace)) + + steps := []testStep{ + {name: "CreateResource", testFunc: func(t testing.TB) { + clientSet := GetClients(t) + scenario.CreateResourceFunc(t, clientSet, scenario.Namespace) + }}, + {name: "EncryptWithPrimaryKMS", testFunc: func(t testing.TB) { + TestEncryptionType(ctx, t, scenario.BasicScenario, scenario.PrimaryProvider) + }}, + {name: "AssertEncryptedWithPrimary", testFunc: func(t testing.TB) { + clientSet := GetClients(t) + scenario.AssertResourceEncryptedFunc(t, clientSet, scenario.ResourceFunc(t, scenario.Namespace)) + }}, + {name: "OffIdentityAfterPrimary", testFunc: func(t testing.TB) { + TestEncryptionTypeIdentity(ctx, t, scenario.BasicScenario) + }}, + {name: "AssertDecryptedAfterPrimary", testFunc: func(t testing.TB) { + clientSet := GetClients(t) + scenario.AssertResourceNotEncryptedFunc(t, clientSet, scenario.ResourceFunc(t, scenario.Namespace)) + }}, + {name: "EncryptWithSecondaryKMS", testFunc: func(t testing.TB) { + TestEncryptionType(ctx, t, scenario.BasicScenario, scenario.SecondaryProvider) + }}, + {name: "AssertEncryptedWithSecondary", testFunc: func(t testing.TB) { + clientSet := GetClients(t) + scenario.AssertResourceEncryptedFunc(t, clientSet, scenario.ResourceFunc(t, scenario.Namespace)) + }}, + } + + for _, step := range steps { + t.Logf("=== STEP: %s ===", step.name) + step.testFunc(e) + if t.Failed() { + t.Errorf("stopping the test as %q step failed", step.name) + return + } + } +} + // KMSInPlaceUpdateScenario tests that updating an in-place KMS config field // (e.g. kmsPluginImage) takes effect without creating a new encryption key. // The caller supplies Provider (initial valid config) and UpdatedProvider (same config diff --git a/vendor/modules.txt b/vendor/modules.txt index fd65cd4ec..e3e8c8088 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -383,7 +383,7 @@ github.com/openshift/client-go/user/applyconfigurations/internal github.com/openshift/client-go/user/applyconfigurations/user/v1 github.com/openshift/client-go/user/clientset/versioned/scheme github.com/openshift/client-go/user/clientset/versioned/typed/user/v1 -# github.com/openshift/library-go v0.0.0-20260615113748-bc9d4056464b +# github.com/openshift/library-go v0.0.0-20260615113748-bc9d4056464b => github.com/ardaguclu/library-go v0.0.0-20260618055608-feb27829a74c ## explicit; go 1.25.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/apps/deployment @@ -1652,3 +1652,4 @@ sigs.k8s.io/structured-merge-diff/v6/value ## explicit; go 1.22 sigs.k8s.io/yaml # github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20251001123353-fd5b1fb35db1 +# github.com/openshift/library-go => github.com/ardaguclu/library-go v0.0.0-20260618055608-feb27829a74c