diff --git a/go.mod b/go.mod index 58a92ce0f6..797671c5a4 100644 --- a/go.mod +++ b/go.mod @@ -129,3 +129,5 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +replace github.com/openshift/library-go => github.com/ardaguclu/library-go v0.0.0-20251105113502-16d1ed44142d diff --git a/go.sum b/go.sum index 88b9dd6227..16a3bcfdd4 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/ardaguclu/library-go v0.0.0-20251105113502-16d1ed44142d h1:oLR7f9rnuK972UaSs9MqWy9muh5hSl6RL9/CIbEkzDQ= +github.com/ardaguclu/library-go v0.0.0-20251105113502-16d1ed44142d/go.mod h1:OlFFws1AO51uzfc48MsStGE4SFMWlMZD0+f5a/zCtKI= 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= @@ -166,8 +168,6 @@ github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+S github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM= -github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 h1:bANtDc8SgetSK4nQehf59x3+H9FqVJCprgjs49/OTg0= -github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5/go.mod h1:OlFFws1AO51uzfc48MsStGE4SFMWlMZD0+f5a/zCtKI= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 c999f140f0..376e6c1067 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 @@ -10,6 +10,8 @@ import ( "strings" "time" + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/library-go/pkg/operator/encryption/kms" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,6 +41,9 @@ import ( // greater than the last key's ID (the first key has a key ID of 1). const encryptionSecretMigrationInterval = time.Hour * 24 * 7 // one week +// kmsHashesGetter is a function type for getting KMS config and key ID hashes +var kmsHashesGetterFunc func(ctx context.Context, kmsConfig *configv1.KMSConfig) (configHash string, keyIDHash []byte, err error) + // keyController creates new keys if necessary. It // * watches // - secrets in openshift-config-managed @@ -104,6 +109,8 @@ func NewKeyController( secretClient: secretClient, } + kmsHashesGetterFunc = defaultGetKMSHashes + return factory.New(). WithSync(c.sync). WithControllerInstanceName(c.controllerInstanceName). @@ -159,11 +166,21 @@ func (c *keyController) sync(ctx context.Context, syncCtx factory.SyncContext) ( } func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext factory.SyncContext, encryptedGRs []schema.GroupResource) error { - currentMode, externalReason, err := c.getCurrentModeAndExternalReason(ctx) + currentMode, externalReason, kmsConfig, err := c.getCurrentModeAndExternalReason(ctx) if err != nil { return err } + // Compute KMS hashes if using KMS mode + var kmsConfigHash string + var kmsKeyHash []byte + if currentMode == state.KMS && kmsConfig != nil { + kmsConfigHash, kmsKeyHash, err = kmsHashesGetterFunc(ctx, kmsConfig) + if err != nil { + return err + } + } + currentConfig, desiredEncryptionState, secrets, isProgressingReason, err := statemachine.GetEncryptionConfigAndState(ctx, c.deployer, c.secretClient, c.encryptionSecretSelector, encryptedGRs) if err != nil { return err @@ -191,7 +208,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact var commonReason *string for gr, grKeys := range desiredEncryptionState { - latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs) + latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs, kmsKeyHash) if !needed { continue } @@ -218,7 +235,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact sort.Sort(sort.StringSlice(reasons)) internalReason := strings.Join(reasons, ", ") - keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason) + keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason, kmsConfigHash, kmsKeyHash) if err != nil { return fmt.Errorf("failed to create key: %v", err) } @@ -255,8 +272,8 @@ func (c *keyController) validateExistingSecret(ctx context.Context, keySecret *c return nil // we made this key earlier } -func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason string) (*corev1.Secret, error) { - bs := crypto.ModeToNewKeyFunc[currentMode]() +func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason string, kmsConfigHash string, kmsKeyIDHash []byte) (*corev1.Secret, error) { + bs := crypto.ModeToNewKeyFunc[currentMode](kmsKeyIDHash) ks := state.KeyState{ Key: apiserverv1.Key{ Name: fmt.Sprintf("%d", keyID), @@ -265,40 +282,55 @@ func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, Mode: currentMode, InternalReason: internalReason, ExternalReason: externalReason, + KMSConfigHash: kmsConfigHash, } return secrets.FromKeyState(c.instanceName, ks) } -func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, error) { +// defaultGetKMSHashes is the default implementation of getting KMS hashes +func defaultGetKMSHashes(ctx context.Context, kmsConfig *configv1.KMSConfig) (string, []byte, error) { + _, configHash, err := kms.GenerateUnixSocketPath(kmsConfig) + if err != nil { + return "", nil, fmt.Errorf("failed to generate KMS unix socket path: %w", err) + } + + // TODO: We'll also need to obtain keyId from Status endpoint of KMS plugin to track the key rotation. + keyId := "static-key-id" + return configHash, kms.ComputeKMSKeyHash(configHash, keyId), nil +} + +func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, *configv1.KMSConfig, error) { apiServer, err := c.apiServerClient.Get(ctx, "cluster", metav1.GetOptions{}) if err != nil { - return "", "", err + return "", "", nil, err } operatorSpec, _, _, err := c.operatorClient.GetOperatorState() if err != nil { - return "", "", err + return "", "", nil, err } encryptionConfig, err := structuredUnsupportedConfigFrom(operatorSpec.UnsupportedConfigOverrides.Raw, c.unsupportedConfigPrefix) if err != nil { - return "", "", err + return "", "", nil, err } reason := encryptionConfig.Encryption.Reason switch currentMode := state.Mode(apiServer.Spec.Encryption.Type); currentMode { case state.AESCBC, state.AESGCM, state.Identity: // secretbox is disabled for now - return currentMode, reason, nil + return currentMode, reason, nil, nil + case state.KMS: + return currentMode, reason, apiServer.Spec.Encryption.KMS, nil case "": // unspecified means use the default (which can change over time) - return state.DefaultMode, reason, nil + return state.DefaultMode, reason, nil, nil default: - return "", "", fmt.Errorf("unknown encryption mode configured: %s", currentMode) + return "", "", nil, fmt.Errorf("unknown encryption mode configured: %s", currentMode) } } // 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, kmsKeyHash []byte) (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 @@ -346,6 +378,17 @@ func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, extern return latestKeyID, "external-reason-changed", true } + // if we are using KMS, check if the KMS configuration or key ID hash has changed + if currentMode == state.KMS { + if latestKey.Key.Secret != base64.StdEncoding.EncodeToString(kmsKeyHash) { + return latestKeyID, "kms-key-changed", true + } + + // For KMS mode, we don't do time-based rotation + // KMS keys are rotated externally by the KMS system + return 0, "", false + } + // we check for encryptionSecretMigratedTimestamp set by migration controller to determine when migration completed // this also generates back pressure for key rotation when migration takes a long time or was recently completed return latestKeyID, "rotation-interval-has-passed", time.Since(latestKey.Migrated.Timestamp) > encryptionSecretMigrationInterval diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go index a623d30f79..443575b3c6 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/crypto/keys.go @@ -7,15 +7,16 @@ import ( ) var ( - ModeToNewKeyFunc = map[state.Mode]func() []byte{ + ModeToNewKeyFunc = map[state.Mode]func(externalKey []byte) []byte{ state.AESCBC: NewAES256Key, state.AESGCM: NewAES256Key, state.SecretBox: NewAES256Key, // secretbox requires a 32 byte key so we can reuse the same function here state.Identity: NewIdentityKey, + state.KMS: NewKMSKey, } ) -func NewAES256Key() []byte { +func NewAES256Key(_ []byte) []byte { b := make([]byte, 32) // AES-256 == 32 byte key if _, err := rand.Read(b); err != nil { panic(err) // rand should never fail @@ -23,6 +24,12 @@ func NewAES256Key() []byte { return b } -func NewIdentityKey() []byte { +func NewIdentityKey(_ []byte) []byte { return make([]byte, 16) // the key is not used to perform encryption but must be a valid AES key } + +// NewKMSKey just stores the hashsum of configurations to track the any changes. +// It does not store any confidential value. +func NewKMSKey(externalKey []byte) []byte { + return externalKey +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go index 3082aa653f..a199b836e1 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig/config.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "sort" + "github.com/openshift/library-go/pkg/operator/encryption/kms" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" apiserverconfigv1 "k8s.io/apiserver/pkg/apis/apiserver/v1" @@ -15,7 +16,7 @@ import ( ) var ( - emptyStaticIdentityKey = base64.StdEncoding.EncodeToString(crypto.NewIdentityKey()) + emptyStaticIdentityKey = base64.StdEncoding.EncodeToString(crypto.NewIdentityKey(nil)) ) // FromEncryptionState converts state to config. @@ -25,7 +26,7 @@ func FromEncryptionState(encryptionState map[schema.GroupResource]state.GroupRes for gr, grKeys := range encryptionState { resourceConfigs = append(resourceConfigs, apiserverconfigv1.ResourceConfiguration{ Resources: []string{gr.String()}, // we are forced to lose data here because this API is broken - Providers: stateToProviders(grKeys), + Providers: stateToProviders(gr.Resource, grKeys), }) } @@ -105,7 +106,21 @@ func ToEncryptionState(encryptionConfig *apiserverconfigv1.EncryptionConfigurati Key: provider.AESGCM.Keys[0], Mode: s, } + case provider.KMS != nil: + configHash, keyHash, keyName, err := kms.ExtractKMSHashAndKeyName(provider) + if err != nil { + klog.Warningf("skipping invalid encryption KMS config for resource %v", provider) + continue // should never happen + } + ks = state.KeyState{ + Key: apiserverconfigv1.Key{ + Name: keyName, + Secret: keyHash, + }, + Mode: state.KMS, + KMSConfigHash: configHash, + } default: klog.Infof("skipping invalid provider index %d for resource %s", i, resourceConfig.Resources[0]) continue // should never happen @@ -139,7 +154,7 @@ func ToEncryptionState(encryptionConfig *apiserverconfigv1.EncryptionConfigurati // it primarily handles the conversion of KeyState to the appropriate provider config. // the identity mode is transformed into a custom aesgcm provider that simply exists to // curry the associated null key secret through the encryption state machine. -func stateToProviders(desired state.GroupResourceState) []apiserverconfigv1.ProviderConfiguration { +func stateToProviders(resource string, desired state.GroupResourceState) []apiserverconfigv1.ProviderConfiguration { allKeys := desired.ReadKeys providers := make([]apiserverconfigv1.ProviderConfiguration, 0, len(allKeys)+1) // one extra for identity @@ -192,6 +207,8 @@ func stateToProviders(desired state.GroupResourceState) []apiserverconfigv1.Prov Keys: []apiserverconfigv1.Key{key.Key}, }, }) + case state.KMS: + providers = append(providers, kms.GenerateKMSProviderConfigurationFromKey(resource, key)) default: // this should never happen because our input should always be valid klog.Infof("skipping key %s as it has invalid mode %s", key.Key.Name, key.Mode) diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms.go new file mode 100644 index 0000000000..60c0b9892c --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/kms/kms.go @@ -0,0 +1,142 @@ +package kms + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "regexp" + "time" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/library-go/pkg/operator/encryption/state" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apiserver/pkg/apis/apiserver/v1" +) + +const ( + // unixSocketBaseDir is the base directory for KMS unix sockets + unixSocketBaseDir = "unix:///var/run/kms" +) + +// GenerateUnixSocketPath generates a unique unix socket path from KMS configuration +// by hashing the provider-specific configuration. +// Returns the socket path and the hash value (first 16 characters). +func GenerateUnixSocketPath(kmsConfig *configv1.KMSConfig) (string, string, error) { + if kmsConfig == nil { + return "", "", fmt.Errorf("kmsConfig cannot be nil") + } + + switch kmsConfig.Type { + case configv1.AWSKMSProvider: + if kmsConfig.AWS == nil { + return "", "", fmt.Errorf("AWS KMS config cannot be nil for AWS provider type") + } + return generateAWSUnixSocketPath(kmsConfig.AWS) + default: + return "", "", fmt.Errorf("unsupported KMS provider type: %s", kmsConfig.Type) + } +} + +// generateAWSUnixSocketPath generates a unique unix socket path from AWS KMS configuration +// by hashing the ARN and region. Returns the socket path and the hash (first 16 characters). +func generateAWSUnixSocketPath(awsConfig *configv1.AWSKMSConfig) (string, string, error) { + if awsConfig.KeyARN == "" { + return "", "", fmt.Errorf("AWS KMS KeyARN cannot be empty") + } + + if awsConfig.Region == "" { + return "", "", fmt.Errorf("AWS region cannot be empty") + } + + // Combine KeyARN and region for hashing + combined := awsConfig.KeyARN + ":" + awsConfig.Region + + // Compute SHA256 hash + hash := sha256.Sum256([]byte(combined)) + hashStr := hex.EncodeToString(hash[:]) + + // Take first 16 characters of hash for shorter path + shortHash := hashStr[:16] + + socketPath := fmt.Sprintf("%s/kms-%s.sock", unixSocketBaseDir, shortHash) + + return socketPath, shortHash, nil +} + +// ComputeKMSKeyHash computes a hash of the KMS key ID returned from the Status endpoint. +// Returns the first 32 characters of the SHA256 hash. +func ComputeKMSKeyHash(configHash, keyID string) []byte { + if keyID == "" { + return nil + } + + combined := configHash + ":" + keyID + hash := sha256.Sum256([]byte(combined)) + hashStr := hex.EncodeToString(hash[:]) + + return []byte(hashStr[:32]) +} + +var ( + // endpointHashRegex matches the config hash in endpoint path: unix://var/run/kms/kms-{configHash16}.sock + endpointHashRegex = regexp.MustCompile(`kms-([a-f0-9]{16})\.sock$`) + // providerNameRegex matches the key ID hash, key ID, and resource in provider name: kms-provider-{keyIDHash32}-{keyID}-{resource} + // Example: kms-provider-abcdef1234567890abcdef1234567890-1-secrets + providerNameRegex = regexp.MustCompile(`^kms-provider-([a-f0-9]{32})-([^-]+)-(.+)$`) +) + +// ExtractKMSHashAndKeyName extracts the KMSConfigHash, KMSKeyIDHash, and key.Name embedded into provider +// name and socket path. Returns (configHash, keyIDHash, keyName, error) +func ExtractKMSHashAndKeyName(provider v1.ProviderConfiguration) (string, string, string, error) { + // Extract the config hash from the endpoint path: unix://var/run/kms/kms-{configHash}.sock + endpoint := provider.KMS.Endpoint + var configHash string + if matches := endpointHashRegex.FindStringSubmatch(endpoint); len(matches) == 2 { + configHash = matches[1] + } else { + return "", "", "", fmt.Errorf("invalid KMS endpoint format: %s", endpoint) + } + + // Extract the key ID hash, key ID, and resource from the provider name: kms-provider-{keyIDHash32}-{keyID}-{resource} + // Example: kms-provider-abcdef1234567890abcdef1234567890-1-secrets + var keyHash, keyName string + providerName := provider.KMS.Name + if matches := providerNameRegex.FindStringSubmatch(providerName); len(matches) == 4 { + keyHash = matches[1] + keyName = matches[2] + // matches[3] is the resource, but we don't need to return it + } else { + return "", "", "", fmt.Errorf("invalid KMS provider name format: %s", providerName) + } + + return configHash, base64.StdEncoding.EncodeToString([]byte(keyHash)), keyName, nil +} + +// GenerateKMSProviderConfigurationFromKey generates the compatible ProviderConfiguration with +// opinionated and extractable fields. We embed: +// - KMSConfigHash in the socket path (endpoint) +// - KMSKeyIDHash, key.Name, and resource in the provider name +// This allows us to extract all three values and detect both config changes and key rotations. +// The resource parameter ensures uniqueness when the same KMS config is used for multiple resources. +func GenerateKMSProviderConfigurationFromKey(resource string, key state.KeyState) v1.ProviderConfiguration { + // Embed KMSConfigHash in the endpoint so we can extract it + // This must generate the same format as GenerateUnixSocketPath + socketPath := fmt.Sprintf("%s/kms-%s.sock", unixSocketBaseDir, key.KMSConfigHash) + // Embed KMSKeyIDHash, key ID, and resource in the provider name so we can extract them when reading back + // Format: kms-provider-{keyIDHash32}-{keyID}-{resource} + // This must match the providerNameRegex + decoded, _ := base64.StdEncoding.DecodeString(key.Key.Secret) + providerName := fmt.Sprintf("kms-provider-%s-%s-%s", decoded, key.Key.Name, resource) + + return v1.ProviderConfiguration{ + KMS: &v1.KMSConfiguration{ + APIVersion: "v2", + Name: providerName, + Endpoint: socketPath, + Timeout: &metav1.Duration{ + Duration: 10 * time.Second, + }, + }, + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go index 4e54317c7d..e3972d3e34 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/secrets.go @@ -57,10 +57,13 @@ func ToKeyState(s *corev1.Secret) (state.KeyState, error) { if v, ok := s.Annotations[encryptionSecretExternalReason]; ok && len(v) > 0 { key.ExternalReason = v } + if v, ok := s.Annotations[EncryptionSecretKMSConfigHash]; ok && len(v) > 0 { + key.KMSConfigHash = v + } keyMode := state.Mode(s.Annotations[encryptionSecretMode]) switch keyMode { - case state.AESCBC, state.AESGCM, state.SecretBox, state.Identity: + case state.AESCBC, state.AESGCM, state.SecretBox, state.Identity, state.KMS: key.Mode = keyMode default: return state.KeyState{}, fmt.Errorf("secret %s/%s has invalid mode: %s", s.Namespace, s.Name, keyMode) @@ -112,6 +115,9 @@ func FromKeyState(component string, ks state.KeyState) (*corev1.Secret, error) { } s.Annotations[EncryptionSecretMigratedResources] = string(bs) } + if len(ks.KMSConfigHash) > 0 { + s.Annotations[EncryptionSecretKMSConfigHash] = ks.KMSConfigHash + } return s, nil } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go index 7161e4a124..e12a87ead9 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/secrets/types.go @@ -49,6 +49,10 @@ const ( // by the encryption controllers. Its sole purpose is to prevent the accidental // deletion of secrets by enforcing a two phase delete. EncryptionSecretFinalizer = "encryption.apiserver.operator.openshift.io/deletion-protection" + + // EncryptionSecretKMSConfigHash is the annotation that stores the hash of the KMS configuration. + // This is used to detect changes in the KMS configuration that would require a new key. + EncryptionSecretKMSConfigHash = "encryption.apiserver.operator.openshift.io/kms-config-hash" ) // MigratedGroupResources is the data structured stored in the diff --git a/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go b/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go index 460c21bfa2..8d2c621f5e 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/encryption/state/types.go @@ -40,6 +40,8 @@ type KeyState struct { InternalReason string // the user via unsupportConfigOverrides.encryption.reason triggered this key. ExternalReason string + // hash of the KMS configuration to detect changes + KMSConfigHash string } type MigrationState struct { @@ -60,6 +62,7 @@ const ( AESGCM Mode = "aesgcm" SecretBox Mode = "secretbox" // available from the first release, see defaultMode below Identity Mode = "identity" // available from the first release, see defaultMode below + KMS Mode = "KMS" // Changing this value requires caution to not break downgrades. // Specifically, if some new Mode is released in version X, that new Mode cannot diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go index 88bd00b251..0b52c3a324 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go @@ -415,3 +415,51 @@ func ApplyValidatingAdmissionPolicyBindingV1(ctx context.Context, client admissi cache.UpdateCachedResourceMetadata(requiredOriginal, actual) return actual, true, nil } + +func DeleteValidatingAdmissionPolicyV1beta1(ctx context.Context, client admissionregistrationclientv1beta1.ValidatingAdmissionPoliciesGetter, recorder events.Recorder, required *admissionregistrationv1beta1.ValidatingAdmissionPolicy) (*admissionregistrationv1beta1.ValidatingAdmissionPolicy, bool, error) { + err := client.ValidatingAdmissionPolicies().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyBindingV1beta1(ctx context.Context, client admissionregistrationclientv1beta1.ValidatingAdmissionPolicyBindingsGetter, recorder events.Recorder, required *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding) (*admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding, bool, error) { + err := client.ValidatingAdmissionPolicyBindings().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyV1(ctx context.Context, client admissionregistrationclientv1.ValidatingAdmissionPoliciesGetter, recorder events.Recorder, required *admissionregistrationv1.ValidatingAdmissionPolicy) (*admissionregistrationv1.ValidatingAdmissionPolicy, bool, error) { + err := client.ValidatingAdmissionPolicies().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyBindingV1(ctx context.Context, client admissionregistrationclientv1.ValidatingAdmissionPolicyBindingsGetter, recorder events.Recorder, required *admissionregistrationv1.ValidatingAdmissionPolicyBinding) (*admissionregistrationv1.ValidatingAdmissionPolicyBinding, bool, error) { + err := client.ValidatingAdmissionPolicyBindings().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go index 9105464bd0..58f49823f3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go @@ -147,7 +147,7 @@ func ApplyDirectly(ctx context.Context, clients *ClientHolder, recorder events.R if clients.kubeClient == nil { result.Error = fmt.Errorf("missing kubeClient") } else { - result.Result, result.Changed, result.Error = ApplyNetworkPolicy(ctx, clients.kubeClient.NetworkingV1(), recorder, t) + result.Result, result.Changed, result.Error = ApplyNetworkPolicy(ctx, clients.kubeClient.NetworkingV1(), recorder, t, cache) } case *rbacv1.ClusterRole: if clients.kubeClient == nil { @@ -380,6 +380,30 @@ func DeleteAll(ctx context.Context, clients *ClientHolder, recorder events.Recor } else { _, result.Changed, result.Error = DeleteValidatingWebhookConfiguration(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) } + case *admissionregistrationv1beta1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t) + } + case *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyBindingV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t) + } + case *admissionregistrationv1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) + } + case *admissionregistrationv1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyBindingV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) + } case *storagev1.CSIDriver: if clients.kubeClient == nil { result.Error = fmt.Errorf("missing kubeClient") diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go index 0a3df326e4..cc2de17ff3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go @@ -15,34 +15,44 @@ import ( "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" ) -// ApplyClusterRole merges objectmeta, does not worry about anything else -func ApplyNetworkPolicy(ctx context.Context, client networkingclientv1.NetworkPoliciesGetter, recorder events.Recorder, required *networkingv1.NetworkPolicy) (*networkingv1.NetworkPolicy, bool, error) { +// ApplyNetworkPolicy merges objectmeta and requires spec +func ApplyNetworkPolicy(ctx context.Context, client networkingclientv1.NetworkPoliciesGetter, recorder events.Recorder, required *networkingv1.NetworkPolicy, cache ResourceCache) (*networkingv1.NetworkPolicy, bool, error) { existing, err := client.NetworkPolicies(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.NetworkPolicies(required.Namespace).Create( ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*networkingv1.NetworkPolicy), metav1.CreateOptions{}) resourcehelper.ReportCreateEvent(recorder, required, err) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } + if cache.SafeToSkipApply(required, existing) { + return existing, false, nil + } + modified := false existingCopy := existing.DeepCopy() resourcemerge.EnsureObjectMeta(&modified, &existingCopy.ObjectMeta, required.ObjectMeta) - if equality.Semantic.DeepEqual(existingCopy.Spec, required.Spec) && !modified { + specContentSame := equality.Semantic.DeepEqual(existingCopy.Spec, required.Spec) + if specContentSame && !modified { + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } + existingCopy.Spec = required.Spec + if klog.V(2).Enabled() { klog.Infof("NetworkPolicy %q changes: %v", required.Name, JSONPatchNoError(existing, existingCopy)) } actual, err := client.NetworkPolicies(existingCopy.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) resourcehelper.ReportUpdateEvent(recorder, required, err) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go index 4d835dd1e2..ed2a96c3d2 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go @@ -22,17 +22,17 @@ import ( "k8s.io/klog/v2" ) -// RevisionController is a controller that watches a set of configmaps and secrets and them against a revision snapshot -// of them. If the original resources changes, the revision counter is increased, stored in LatestAvailableRevision -// field of the operator config and new snapshots suffixed by the revision are created. +// RevisionController monitors configmaps and secrets, comparing them against their revision snapshots. +// When these resources change, it increments the revision counter, updates the LatestAvailableRevision +// field in the operator config, and creates new revision-suffixed snapshots. type RevisionController struct { controllerInstanceName string targetNamespace string - // configMaps is the list of configmaps that are directly copied.A different actor/controller modifies these. + // configMaps is the list of configmaps that are directly copied. A different actor/controller modifies these. // the first element should be the configmap that contains the static pod manifest configMaps []RevisionResource - // secrets is a list of secrets that are directly copied for the current values. A different actor/controller modifies these. + // secrets is a list of secrets that are directly copied for the current values. A different actor/controller modifies these. secrets []RevisionResource operatorClient v1helpers.OperatorClient @@ -97,7 +97,7 @@ func NewRevisionController( } // createRevisionIfNeeded takes care of creating content for the static pods to use. -// returns whether or not requeue and if an error happened when updating status. Normally it updates status itself. +// returns whether or not requeue and if an error happened when updating status. Normally it updates status itself. func (c RevisionController) createRevisionIfNeeded(ctx context.Context, recorder events.Recorder, currentLastAvailableRevision int32) error { isLatestRevisionCurrent, requiredIsNotFound, reason := c.isLatestRevisionCurrent(ctx, currentLastAvailableRevision) @@ -367,9 +367,9 @@ func (c RevisionController) sync(ctx context.Context, syncCtx factory.SyncContex } // If the operator status's latest available revision is not the same as the observed latest revision, update the operator. - // This needs to be done even if the revision precondition is not required because it ensures our operator status is + // This needs to be done even if the revision precondition is not met because it ensures our operator status is // correct for all consumers. - // This is what is going to allow us to move where the state is stored in authentications.openshift.io. + // This ensures the operator status accurately reflects the latest revision. latestObservedRevision, err := c.getLatestAvailableRevision(ctx) if err != nil { return err diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go index 111776d994..6911454be0 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go @@ -2,10 +2,12 @@ package certsyncpod import ( "context" + "fmt" "os" "path/filepath" "reflect" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -17,17 +19,19 @@ import ( "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/events" - "github.com/openshift/library-go/pkg/operator/staticpod" "github.com/openshift/library-go/pkg/operator/staticpod/controller/installer" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir" ) +const stagingDirUID = "cert-sync" + type CertSyncController struct { destinationDir string namespace string configMaps []installer.UnrevisionedResource secrets []installer.UnrevisionedResource - configmapGetter corev1interface.ConfigMapInterface + configMapGetter corev1interface.ConfigMapInterface configMapLister v1.ConfigMapLister secretGetter corev1interface.SecretInterface secretLister v1.SecretLister @@ -42,10 +46,10 @@ func NewCertSyncController(targetDir, targetNamespace string, configmaps, secret secrets: secrets, eventRecorder: eventRecorder.WithComponentSuffix("cert-sync-controller"), - configmapGetter: kubeClient.CoreV1().ConfigMaps(targetNamespace), + configMapGetter: kubeClient.CoreV1().ConfigMaps(targetNamespace), configMapLister: informers.Core().V1().ConfigMaps().Lister(), - secretLister: informers.Core().V1().Secrets().Lister(), secretGetter: kubeClient.CoreV1().Secrets(targetNamespace), + secretLister: informers.Core().V1().Secrets().Lister(), } return factory.New(). @@ -60,15 +64,12 @@ func NewCertSyncController(targetDir, targetNamespace string, configmaps, secret ) } -func getConfigMapDir(targetDir, configMapName string) string { - return filepath.Join(targetDir, "configmaps", configMapName) -} - -func getSecretDir(targetDir, secretName string) string { - return filepath.Join(targetDir, "secrets", secretName) -} - func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncContext) error { + if err := os.RemoveAll(getStagingDir(c.destinationDir)); err != nil { + c.eventRecorder.Warningf("CertificateUpdateFailed", fmt.Sprintf("Failed to prune staging directory: %v", err)) + return err + } + errors := []error{} klog.Infof("Syncing configmaps: %v", c.configMaps) @@ -80,7 +81,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue case apierrors.IsNotFound(err) && cm.Optional: - configMapFile := getConfigMapDir(c.destinationDir, cm.Name) + configMapFile := getConfigMapTargetDir(c.destinationDir, cm.Name) if _, err := os.Stat(configMapFile); os.IsNotExist(err) { // if the configmap file does not exist, there is no work to do, so skip making any live check and just return. // if the configmap actually exists in the API, we'll eventually see it on the watch. @@ -88,7 +89,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte } // Check with the live call it is really missing - configMap, err = c.configmapGetter.Get(ctx, cm.Name, metav1.GetOptions{}) + configMap, err = c.configMapGetter.Get(ctx, cm.Name, metav1.GetOptions{}) if err == nil { klog.Infof("Caches are stale. They don't see configmap '%s/%s', yet it is present", configMap.Namespace, configMap.Name) // We will get re-queued when we observe the change @@ -113,9 +114,10 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - contentDir := getConfigMapDir(c.destinationDir, cm.Name) + contentDir := getConfigMapTargetDir(c.destinationDir, cm.Name) + stagingDir := getConfigMapStagingDir(c.destinationDir, cm.Name) - data := map[string]string{} + data := make(map[string]string, len(configMap.Data)) for filename := range configMap.Data { fullFilename := filepath.Join(contentDir, filename) @@ -138,7 +140,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte klog.V(2).Infof("Syncing updated configmap '%s/%s'.", configMap.Namespace, configMap.Name) // We need to do a live get here so we don't overwrite a newer file with one from a stale cache - configMap, err = c.configmapGetter.Get(ctx, configMap.Name, metav1.GetOptions{}) + configMap, err = c.configMapGetter.Get(ctx, configMap.Name, metav1.GetOptions{}) if err != nil { // Even if the error is not exists we will act on it when caches catch up c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed getting configmap: %s/%s: %v", c.namespace, cm.Name, err) @@ -152,27 +154,11 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil && !os.IsExist(err) { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating directory for configmap: %s/%s: %v", configMap.Namespace, configMap.Name, err) - errors = append(errors, err) - continue - } - for filename, content := range configMap.Data { - fullFilename := filepath.Join(contentDir, filename) - // if the existing is the same, do nothing - if reflect.DeepEqual(data[fullFilename], content) { - continue - } - - klog.Infof("Writing configmap manifest %q ...", fullFilename) - if err := staticpod.WriteFileAtomic([]byte(content), 0644, fullFilename); err != nil { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed writing file for configmap: %s/%s: %v", configMap.Namespace, configMap.Name, err) - errors = append(errors, err) - continue - } + files := make(map[string][]byte, len(configMap.Data)) + for k, v := range configMap.Data { + files[k] = []byte(v) } - c.eventRecorder.Eventf("CertificateUpdated", "Wrote updated configmap: %s/%s", configMap.Namespace, configMap.Name) + errors = append(errors, syncDirectory(c.eventRecorder, "configmap", configMap.ObjectMeta, contentDir, 0755, stagingDir, files, 0644)) } klog.Infof("Syncing secrets: %v", c.secrets) @@ -184,7 +170,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue case apierrors.IsNotFound(err) && s.Optional: - secretFile := getSecretDir(c.destinationDir, s.Name) + secretFile := getSecretTargetDir(c.destinationDir, s.Name) if _, err := os.Stat(secretFile); os.IsNotExist(err) { // if the secret file does not exist, there is no work to do, so skip making any live check and just return. // if the secret actually exists in the API, we'll eventually see it on the watch. @@ -218,9 +204,10 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - contentDir := getSecretDir(c.destinationDir, s.Name) + contentDir := getSecretTargetDir(c.destinationDir, s.Name) + stagingDir := getSecretStagingDir(c.destinationDir, s.Name) - data := map[string][]byte{} + data := make(map[string][]byte, len(secret.Data)) for filename := range secret.Data { fullFilename := filepath.Join(contentDir, filename) @@ -257,29 +244,50 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil && !os.IsExist(err) { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating directory for secret: %s/%s: %v", secret.Namespace, secret.Name, err) - errors = append(errors, err) - continue - } - for filename, content := range secret.Data { - // TODO fix permissions - fullFilename := filepath.Join(contentDir, filename) - // if the existing is the same, do nothing - if reflect.DeepEqual(data[fullFilename], content) { - continue - } + errors = append(errors, syncDirectory(c.eventRecorder, "secret", secret.ObjectMeta, contentDir, 0755, stagingDir, secret.Data, 0600)) + } + return utilerrors.NewAggregate(errors) +} - klog.Infof("Writing secret manifest %q ...", fullFilename) - if err := staticpod.WriteFileAtomic(content, 0600, fullFilename); err != nil { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed writing file for secret: %s/%s: %v", secret.Namespace, secret.Name, err) - errors = append(errors, err) - continue - } +func syncDirectory( + eventRecorder events.Recorder, + typeName string, o metav1.ObjectMeta, + targetDir string, targetDirPerm os.FileMode, stagingDir string, + fileContents map[string][]byte, filePerm os.FileMode, +) error { + files := make(map[string]types.File, len(fileContents)) + for filename, content := range fileContents { + files[filename] = types.File{ + Content: content, + Perm: filePerm, } - c.eventRecorder.Eventf("CertificateUpdated", "Wrote updated secret: %s/%s", secret.Namespace, secret.Name) } - return utilerrors.NewAggregate(errors) + if err := atomicdir.Sync(targetDir, targetDirPerm, stagingDir, files); err != nil { + err = fmt.Errorf("failed to sync %s %s/%s (directory %q): %w", typeName, o.Name, o.Namespace, targetDir, err) + eventRecorder.Warning("CertificateUpdateFailed", err.Error()) + return err + } + eventRecorder.Eventf("CertificateUpdated", "Wrote updated %s: %s/%s", typeName, o.Namespace, o.Name) + return nil +} + +func getStagingDir(targetDir string) string { + return filepath.Join(targetDir, "staging", stagingDirUID) +} + +func getConfigMapTargetDir(targetDir, configMapName string) string { + return filepath.Join(targetDir, "configmaps", configMapName) +} + +func getConfigMapStagingDir(targetDir, configMapName string) string { + return filepath.Join(getStagingDir(targetDir), "configmaps", configMapName) +} + +func getSecretTargetDir(targetDir, secretName string) string { + return filepath.Join(targetDir, "secrets", secretName) +} + +func getSecretStagingDir(targetDir, secretName string) string { + return filepath.Join(getStagingDir(targetDir), "secrets", secretName) } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml index 657c87ec8b..29d4a345b3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml @@ -14,6 +14,7 @@ spec: terminationGracePeriodSeconds: 3 tolerations: - operator: Exists + hostUsers: false containers: - name: guard image: # Value set by operator diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go index 31afadff86..35424a56f5 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go @@ -5,38 +5,40 @@ import ( "fmt" "os" "path" + "path/filepath" "sort" "strconv" "strings" "time" - "k8s.io/utils/clock" - - "k8s.io/apimachinery/pkg/util/wait" - "github.com/blang/semver/v4" "github.com/davecgh/go-spew/spew" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types" "github.com/spf13/cobra" "github.com/spf13/pflag" - "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/server" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/klog/v2" + "k8s.io/utils/clock" "github.com/openshift/library-go/pkg/config/client" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourceread" "github.com/openshift/library-go/pkg/operator/resource/retry" - "github.com/openshift/library-go/pkg/operator/staticpod" "github.com/openshift/library-go/pkg/operator/staticpod/internal" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir" "github.com/openshift/library-go/pkg/operator/staticpod/internal/flock" ) +const stagingDirUID = "installer" + type InstallOptions struct { // TODO replace with genericclioptions KubeConfig string @@ -219,8 +221,10 @@ func (o *InstallOptions) kubeletVersion(ctx context.Context) (string, error) { func (o *InstallOptions) copySecretsAndConfigMaps(ctx context.Context, resourceDir string, secretNames, optionalSecretNames, configNames, optionalConfigNames sets.Set[string], prefixed bool) error { - klog.Infof("Creating target resource directory %q ...", resourceDir) - if err := os.MkdirAll(resourceDir, 0755); err != nil && !os.IsExist(err) { + + stagingDirBase := getStagingDir(resourceDir) + klog.Infof("Pruning staging directory %q ...", stagingDirBase) + if err := os.RemoveAll(stagingDirBase); err != nil { return err } @@ -258,34 +262,43 @@ func (o *InstallOptions) copySecretsAndConfigMaps(ctx context.Context, resourceD if prefixed { secretBaseName = o.prefixFor(secret.Name) } - contentDir := path.Join(resourceDir, "secrets", secretBaseName) - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil { - return err - } - for filename, content := range secret.Data { - if err := writeSecret(content, path.Join(contentDir, filename)); err != nil { - return err + + contentDir := getSecretTargetDir(resourceDir, secretBaseName) + stagingDir := getSecretStagingDir(resourceDir, secretBaseName) + + files := make(map[string]types.File, len(secret.Data)) + for name, content := range secret.Data { + files[name] = types.File{ + Content: content, + Perm: getFilePermissionsSecret(name), } } + + if err := atomicdir.Sync(contentDir, 0755, stagingDir, files); err != nil { + return fmt.Errorf("failed to sync secret %s/%s (directory %q): %w", secret.Namespace, secret.Name, contentDir, err) + } } for _, configmap := range configs { configMapBaseName := configmap.Name if prefixed { configMapBaseName = o.prefixFor(configmap.Name) } - contentDir := path.Join(resourceDir, "configmaps", configMapBaseName) - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil { - return err - } - for filename, content := range configmap.Data { - if err := writeConfig([]byte(content), path.Join(contentDir, filename)); err != nil { - return err + + contentDir := getConfigMapTargetDir(resourceDir, configMapBaseName) + stagingDir := getConfigMapStagingDir(resourceDir, configMapBaseName) + + files := make(map[string]types.File, len(configmap.Data)) + for name, content := range configmap.Data { + files[name] = types.File{ + Content: []byte(content), + Perm: getFilePermissionsConfigMap(name), } } - } + if err := atomicdir.Sync(contentDir, 0755, stagingDir, files); err != nil { + return fmt.Errorf("failed to sync configmap %s/%s (directory %q): %w", configmap.Namespace, configmap.Name, contentDir, err) + } + } return nil } @@ -626,22 +639,36 @@ func (o *InstallOptions) writePod(rawPodBytes []byte, manifestFileName, resource return nil } -func writeConfig(content []byte, fullFilename string) error { - klog.Infof("Writing config file %q ...", fullFilename) +func getStagingDir(targetDir string) string { + return filepath.Join(targetDir, "staging", stagingDirUID) +} - filePerms := os.FileMode(0600) - if strings.HasSuffix(fullFilename, ".sh") { - filePerms = 0755 - } - return staticpod.WriteFileAtomic(content, filePerms, fullFilename) +func getConfigMapTargetDir(targetDir, configMapName string) string { + return filepath.Join(targetDir, "configmaps", configMapName) } -func writeSecret(content []byte, fullFilename string) error { - klog.Infof("Writing secret manifest %q ...", fullFilename) +func getConfigMapStagingDir(targetDir, configMapName string) string { + return filepath.Join(getStagingDir(targetDir), "configmaps", configMapName) +} + +func getSecretTargetDir(targetDir, secretName string) string { + return filepath.Join(targetDir, "secrets", secretName) +} + +func getSecretStagingDir(targetDir, secretName string) string { + return filepath.Join(getStagingDir(targetDir), "secrets", secretName) +} + +func getFilePermissionsConfigMap(filename string) os.FileMode { + if strings.HasSuffix(filename, ".sh") { + return 0755 + } + return 0600 +} - filePerms := os.FileMode(0600) - if strings.HasSuffix(fullFilename, ".sh") { - filePerms = 0700 +func getFilePermissionsSecret(filename string) os.FileMode { + if strings.HasSuffix(filename, ".sh") { + return 0700 } - return staticpod.WriteFileAtomic(content, filePerms, fullFilename) + return 0600 } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go new file mode 100644 index 0000000000..9ce912af76 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go @@ -0,0 +1,22 @@ +//go:build linux + +package atomicdir + +import ( + "golang.org/x/sys/unix" +) + +// swap can be used to exchange two directories atomically. +func swap(firstDir, secondDir string) error { + // Renameat2 can be used to exchange two directories atomically when RENAME_EXCHANGE flag is specified. + // The paths to be exchanged can be specified in multiple ways: + // + // * You can specify a file descriptor and a relative path to that descriptor. + // * You can specify an absolute path, in which case the file descriptor is ignored. + // + // We use AT_FDCWD special file descriptor so that when any of the paths is relative, + // it's relative to the current working directory. + // + // For more details, see `man renameat2` as that is the associated C library function. + return unix.Renameat2(unix.AT_FDCWD, firstDir, unix.AT_FDCWD, secondDir, unix.RENAME_EXCHANGE) +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go new file mode 100644 index 0000000000..6f5c0fed9e --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package atomicdir + +import "errors" + +// swap can be used to exchange two directories atomically. +// +// This function is only implemented for Linux and returns an error on other platforms. +func swap(firstDir, secondDir string) error { + return errors.New("swap is not supported on this platform") +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go new file mode 100644 index 0000000000..0c2cc3dfff --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go @@ -0,0 +1,88 @@ +package atomicdir + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/klog/v2" + + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types" +) + +// Sync can be used to atomically synchronize target directory with the given file content map. +// This is done by populating a staging directory, then atomically swapping it with the target directory. +// This effectively means that any extra files in the target directory are pruned. +// +// The staging directory needs to be explicitly specified. It is initially created using os.MkdirAll with targetDirPerm. +// It is then populated using files with filePerm. Once the atomic swap is performed, the staging directory +// (which is now the original target directory) is removed. +func Sync(targetDir string, targetDirPerm os.FileMode, stagingDir string, files map[string]types.File) error { + return sync(&realFS, targetDir, targetDirPerm, stagingDir, files) +} + +type fileSystem struct { + MkdirAll func(path string, perm os.FileMode) error + RemoveAll func(path string) error + WriteFile func(name string, data []byte, perm os.FileMode) error + SwapDirectories func(dirA, dirB string) error +} + +var realFS = fileSystem{ + MkdirAll: os.MkdirAll, + RemoveAll: os.RemoveAll, + WriteFile: os.WriteFile, + SwapDirectories: swap, +} + +// sync prepares a tmp directory and writes all files into that directory. +// Then it atomically swap the tmp directory for the target one. +// This is currently implemented as really atomically swapping directories. +// +// The same goal of atomic swap could be implemented using symlinks much like AtomicWriter does in +// https://github.com/kubernetes/kubernetes/blob/v1.34.0/pkg/volume/util/atomic_writer.go#L58 +// The reason we don't do that is that we already have a directory populated and watched that needs to we swapped. +// In other words, it's for compatibility reasons. And if we were to migrate to the symlink approach, +// we would anyway need to atomically turn the current data directory into a symlink. +// This would all just increase complexity and require atomic swap on the OS level anyway. +func sync(fs *fileSystem, targetDir string, targetDirPerm os.FileMode, stagingDir string, files map[string]types.File) (retErr error) { + klog.Infof("Ensuring target directory %q exists ...", targetDir) + if err := fs.MkdirAll(targetDir, targetDirPerm); err != nil { + return fmt.Errorf("failed creating target directory: %w", err) + } + + klog.Infof("Creating staging directory %q ...", stagingDir) + if err := fs.MkdirAll(stagingDir, targetDirPerm); err != nil { + return fmt.Errorf("failed creating staging directory: %w", err) + } + defer func() { + if err := fs.RemoveAll(stagingDir); err != nil { + if retErr != nil { + retErr = fmt.Errorf("failed removing staging directory %q: %w; previous error: %w", stagingDir, err, retErr) + return + } + retErr = fmt.Errorf("failed removing staging directory %q: %w", stagingDir, err) + } + }() + + for filename, file := range files { + // Make sure filename is a plain filename, not a path. + // This also ensures the staging directory cannot be escaped. + if filename != filepath.Base(filename) { + return fmt.Errorf("filename cannot be a path: %q", filename) + } + + fullFilename := filepath.Join(stagingDir, filename) + klog.Infof("Writing file %q ...", fullFilename) + + if err := fs.WriteFile(fullFilename, file.Content, file.Perm); err != nil { + return fmt.Errorf("failed writing %q: %w", fullFilename, err) + } + } + + klog.Infof("Atomically swapping staging directory %q with target directory %q ...", stagingDir, targetDir) + if err := fs.SwapDirectories(targetDir, stagingDir); err != nil { + return fmt.Errorf("failed swapping target directory %q with staging directory %q: %w", targetDir, stagingDir, err) + } + return +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go new file mode 100644 index 0000000000..f99f41c57c --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go @@ -0,0 +1,10 @@ +// Package types exists to avoid import cycles as it's imported by both atomicdir and atomicdir/testing. +package types + +import "os" + +// File represents file content together with the associated permissions. +type File struct { + Content []byte + Perm os.FileMode +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7ca5a876b7..6ff3065914 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -349,7 +349,7 @@ github.com/openshift/client-go/security/informers/externalversions/internalinter github.com/openshift/client-go/security/informers/externalversions/security github.com/openshift/client-go/security/informers/externalversions/security/v1 github.com/openshift/client-go/security/listers/security/v1 -# github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 +# github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 => github.com/ardaguclu/library-go v0.0.0-20251105113502-16d1ed44142d ## explicit; go 1.24.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/assets @@ -386,6 +386,7 @@ github.com/openshift/library-go/pkg/operator/encryption/controllers/migrators github.com/openshift/library-go/pkg/operator/encryption/crypto github.com/openshift/library-go/pkg/operator/encryption/deployer github.com/openshift/library-go/pkg/operator/encryption/encryptionconfig +github.com/openshift/library-go/pkg/operator/encryption/kms github.com/openshift/library-go/pkg/operator/encryption/observer github.com/openshift/library-go/pkg/operator/encryption/secrets github.com/openshift/library-go/pkg/operator/encryption/state @@ -424,6 +425,8 @@ github.com/openshift/library-go/pkg/operator/staticpod/controller/staticpodfallb github.com/openshift/library-go/pkg/operator/staticpod/controller/staticpodstate github.com/openshift/library-go/pkg/operator/staticpod/installerpod github.com/openshift/library-go/pkg/operator/staticpod/internal +github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir +github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types github.com/openshift/library-go/pkg/operator/staticpod/internal/flock github.com/openshift/library-go/pkg/operator/staticpod/prune github.com/openshift/library-go/pkg/operator/staticpod/startupmonitor @@ -1608,3 +1611,4 @@ sigs.k8s.io/structured-merge-diff/v6/value # sigs.k8s.io/yaml v1.6.0 ## explicit; go 1.22 sigs.k8s.io/yaml +# github.com/openshift/library-go => github.com/ardaguclu/library-go v0.0.0-20251105113502-16d1ed44142d