diff --git a/pkg/operator/encryption/kms/pluginlifecycle/sidecar.go b/pkg/operator/encryption/kms/pluginlifecycle/sidecar.go index 4bdfd26cb6..2c7e68a8e5 100644 --- a/pkg/operator/encryption/kms/pluginlifecycle/sidecar.go +++ b/pkg/operator/encryption/kms/pluginlifecycle/sidecar.go @@ -40,10 +40,10 @@ type sidecarProvider interface { // newSidecarProvider creates a provider-specific sidecarProvider for the given keyID and plugin configuration, // wiring in credentials via the credentialResolver. -func newSidecarProvider(keyID string, udsPath string, pluginConfig configv1.KMSPluginConfig, creds *credentialResolver) (sidecarProvider, error) { +func newSidecarProvider(keyID string, udsPath string, pluginConfig configv1.KMSPluginConfig, creds *credentialResolver, unsupportedConfig []byte) (sidecarProvider, error) { switch pluginConfig.Type { case configv1.VaultKMSProvider: - return newVaultSidecarProvider("vault-kms-plugin", keyID, udsPath, pluginConfig.Vault, creds) + return newVaultSidecarProvider("vault-kms-plugin", keyID, udsPath, pluginConfig.Vault, creds, unsupportedConfig) default: return nil, fmt.Errorf("unsupported KMS plugin configuration") } @@ -57,11 +57,11 @@ func newSidecarProvider(keyID string, udsPath string, pluginConfig configv1.KMSP // // It is a no-op when the KMSEncryption feature gate is not enabled or the encryption-config secret does not exist. // The secretClient should be uncached to avoid injecting sidecars based on a stale encryption configuration. -func AddKMSPluginSidecarToStaticPodSpec(ctx context.Context, podSpec *corev1.PodSpec, containerName string, encryptionConfigNamespace string, encryptionConfigSecretName string, secretClient corev1client.SecretsGetter, featureGateAccessor featuregates.FeatureGateAccess) error { +func AddKMSPluginSidecarToStaticPodSpec(ctx context.Context, podSpec *corev1.PodSpec, containerName string, encryptionConfigNamespace string, encryptionConfigSecretName string, secretClient corev1client.SecretsGetter, featureGateAccessor featuregates.FeatureGateAccess, unsupportedConfig []byte) error { // The static pod revision controller copies secret data to disk under resourcesDir/secrets//. credentialsDir := filepath.Join(resourcesDir, "secrets", encryptionConfigSecretName) - sidecarNames, err := addKMSPluginSidecars(ctx, podSpec, containerName, encryptionConfigNamespace, encryptionConfigSecretName, secretClient, featureGateAccessor, credentialsDir) + sidecarNames, err := addKMSPluginSidecars(ctx, podSpec, containerName, encryptionConfigNamespace, encryptionConfigSecretName, secretClient, featureGateAccessor, credentialsDir, unsupportedConfig) if err != nil { return err } @@ -89,8 +89,8 @@ func AddKMSPluginSidecarToStaticPodSpec(ctx context.Context, podSpec *corev1.Pod // // It is a no-op when the KMSEncryption feature gate is not enabled or the encryption-config secret does not exist. // The secretClient should be uncached to avoid injecting sidecars based on a stale encryption configuration. -func AddKMSPluginSidecarToPodSpec(ctx context.Context, podSpec *corev1.PodSpec, containerName string, encryptionConfigNamespace string, encryptionConfigSecretName string, secretClient corev1client.SecretsGetter, featureGateAccessor featuregates.FeatureGateAccess) error { - sidecarNames, err := addKMSPluginSidecars(ctx, podSpec, containerName, encryptionConfigNamespace, encryptionConfigSecretName, secretClient, featureGateAccessor, credentialsMountPath) +func AddKMSPluginSidecarToPodSpec(ctx context.Context, podSpec *corev1.PodSpec, containerName string, encryptionConfigNamespace string, encryptionConfigSecretName string, secretClient corev1client.SecretsGetter, featureGateAccessor featuregates.FeatureGateAccess, unsupportedConfig []byte) error { + sidecarNames, err := addKMSPluginSidecars(ctx, podSpec, containerName, encryptionConfigNamespace, encryptionConfigSecretName, secretClient, featureGateAccessor, credentialsMountPath, unsupportedConfig) if err != nil { return err } @@ -118,7 +118,7 @@ func AddKMSPluginSidecarToPodSpec(ctx context.Context, podSpec *corev1.PodSpec, // addKMSPluginSidecars contains the shared logic for discovering KMS plugins and injecting sidecar containers. // It returns the names of the sidecar containers that were injected, so callers can add deployment-mode-specific volume mounts. -func addKMSPluginSidecars(ctx context.Context, podSpec *corev1.PodSpec, containerName string, encryptionConfigNamespace string, encryptionConfigSecretName string, secretClient corev1client.SecretsGetter, featureGateAccessor featuregates.FeatureGateAccess, credentialsDir string) ([]string, error) { +func addKMSPluginSidecars(ctx context.Context, podSpec *corev1.PodSpec, containerName string, encryptionConfigNamespace string, encryptionConfigSecretName string, secretClient corev1client.SecretsGetter, featureGateAccessor featuregates.FeatureGateAccess, credentialsDir string, unsupportedConfig []byte) ([]string, error) { if podSpec == nil { return nil, fmt.Errorf("pod spec cannot be nil") } @@ -183,7 +183,7 @@ func addKMSPluginSidecars(ctx context.Context, podSpec *corev1.PodSpec, containe keyID: keyID, } - provider, err := newSidecarProvider(keyID, udsPath, pluginConfig, creds) + provider, err := newSidecarProvider(keyID, udsPath, pluginConfig, creds, unsupportedConfig) if err != nil { return nil, fmt.Errorf("failed to create a sidecar provider for keyID %s: %w", keyID, err) } diff --git a/pkg/operator/encryption/kms/pluginlifecycle/sidecar_test.go b/pkg/operator/encryption/kms/pluginlifecycle/sidecar_test.go index 1b58b5c299..6b57a94654 100644 --- a/pkg/operator/encryption/kms/pluginlifecycle/sidecar_test.go +++ b/pkg/operator/encryption/kms/pluginlifecycle/sidecar_test.go @@ -504,7 +504,7 @@ func TestAddKMSPluginSidecarToPodSpec(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := AddKMSPluginSidecarToPodSpec(context.Background(), tt.actualPodSpec, "kube-apiserver", "openshift-kube-apiserver", "encryption-config", tt.secretClient, tt.featureGateAccessor) + err := AddKMSPluginSidecarToPodSpec(context.Background(), tt.actualPodSpec, "kube-apiserver", "openshift-kube-apiserver", "encryption-config", tt.secretClient, tt.featureGateAccessor, nil) if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return @@ -529,7 +529,7 @@ func TestAddKMSPluginSidecarToPodSpecIdempotency(t *testing.T) { call := func() { t.Helper() - err := AddKMSPluginSidecarToPodSpec(context.Background(), podSpec, "kube-apiserver", "openshift-kube-apiserver", "encryption-config", sc, fga) + err := AddKMSPluginSidecarToPodSpec(context.Background(), podSpec, "kube-apiserver", "openshift-kube-apiserver", "encryption-config", sc, fga, nil) require.NoError(t, err) } diff --git a/pkg/operator/encryption/kms/pluginlifecycle/unsupported_config.go b/pkg/operator/encryption/kms/pluginlifecycle/unsupported_config.go new file mode 100644 index 0000000000..8f45893c90 --- /dev/null +++ b/pkg/operator/encryption/kms/pluginlifecycle/unsupported_config.go @@ -0,0 +1,38 @@ +package pluginlifecycle + +import ( + "encoding/json" + + "k8s.io/klog/v2" + + kyaml "k8s.io/apimachinery/pkg/util/yaml" +) + +type unsupportedKMSConfig struct { + Encryption struct { + KMS struct { + Vault struct { + LogLevel string `json:"logLevel"` + } `json:"vault"` + } `json:"kms"` + } `json:"encryption"` +} + +func parseUnsupportedKMSConfig(raw []byte) (unsupportedKMSConfig, error) { + if len(raw) == 0 { + return unsupportedKMSConfig{}, nil + } + + jsonRaw, err := kyaml.ToJSON(raw) + if err != nil { + klog.Warning(err) + return unsupportedKMSConfig{}, err + } + + config := unsupportedKMSConfig{} + if err := json.Unmarshal(jsonRaw, &config); err != nil { + return unsupportedKMSConfig{}, nil + } + + return config, nil +} diff --git a/pkg/operator/encryption/kms/pluginlifecycle/unsupported_config_test.go b/pkg/operator/encryption/kms/pluginlifecycle/unsupported_config_test.go new file mode 100644 index 0000000000..f35b1e0c4f --- /dev/null +++ b/pkg/operator/encryption/kms/pluginlifecycle/unsupported_config_test.go @@ -0,0 +1,64 @@ +package pluginlifecycle + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseUnsupportedKMSConfig(t *testing.T) { + tests := []struct { + name string + raw []byte + expectedLogLevel string + expectError bool + }{ + { + name: "nil input returns empty config", + raw: nil, + expectedLogLevel: "", + }, + { + name: "empty input returns empty config", + raw: []byte{}, + expectedLogLevel: "", + }, + { + name: "JSON with log level", + raw: []byte(`{"encryption":{"kms":{"vault":{"logLevel":"debug-extended"}}}}`), + expectedLogLevel: "debug-extended", + }, + { + name: "YAML with log level", + raw: []byte("encryption:\n kms:\n vault:\n logLevel: trace\n"), + expectedLogLevel: "trace", + }, + { + name: "unrelated fields are ignored", + raw: []byte(`{"encryption":{"reason":"test"}}`), + expectedLogLevel: "", + }, + { + name: "malformed JSON is handled gracefully", + raw: []byte(`{not json`), + expectedLogLevel: "", + }, + { + name: "unparsable input returns error", + raw: []byte{0x00, 0x01, 0x02}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := parseUnsupportedKMSConfig(tt.raw) + if tt.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedLogLevel, config.Encryption.KMS.Vault.LogLevel) + }) + } +} diff --git a/pkg/operator/encryption/kms/pluginlifecycle/vault.go b/pkg/operator/encryption/kms/pluginlifecycle/vault.go index d135b1549e..69d345fc45 100644 --- a/pkg/operator/encryption/kms/pluginlifecycle/vault.go +++ b/pkg/operator/encryption/kms/pluginlifecycle/vault.go @@ -11,7 +11,7 @@ import ( // newVaultSidecarProvider creates a Vault sidecar provider from the given KMS plugin data. // It assumes the input data has been already been validated. -func newVaultSidecarProvider(name, keyID, udsPath string, vaultConfig configv1.VaultKMSPluginConfig, creds *credentialResolver) (*vault, error) { +func newVaultSidecarProvider(name, keyID, udsPath string, vaultConfig configv1.VaultKMSPluginConfig, creds *credentialResolver, unsupportedConfig []byte) (*vault, error) { secretName := vaultConfig.Authentication.AppRole.Secret.Name if secretName == "" { return nil, fmt.Errorf("vault AppRole authentication secret name cannot be empty") @@ -35,6 +35,11 @@ func newVaultSidecarProvider(name, keyID, udsPath string, vaultConfig configv1.V return nil, fmt.Errorf("secret ID path cannot be empty") } + kmsConfig, err := parseUnsupportedKMSConfig(unsupportedConfig) + if err != nil { + return nil, err + } + return &vault{ name: name, keyID: keyID, @@ -42,6 +47,7 @@ func newVaultSidecarProvider(name, keyID, udsPath string, vaultConfig configv1.V config: vaultConfig, roleID: roleID, secretIDPath: secretIDPath, + logLevel: kmsConfig.Encryption.KMS.Vault.LogLevel, }, nil } @@ -53,6 +59,7 @@ type vault struct { config configv1.VaultKMSPluginConfig roleID string secretIDPath string + logLevel string } // Name returns the sidecar name appended by the key id. @@ -77,6 +84,9 @@ func (v *vault) BuildSidecarContainer() (corev1.Container, error) { if v.config.VaultNamespace != "" { args = append(args, fmt.Sprintf("-vault-namespace=%s", v.config.VaultNamespace)) } + if v.logLevel != "" { + args = append(args, fmt.Sprintf("-log-level=%s", v.logLevel)) + } // Temporary workarounds. These should go away as we progress with the feature. args = append(args, diff --git a/pkg/operator/encryption/kms/pluginlifecycle/vault_test.go b/pkg/operator/encryption/kms/pluginlifecycle/vault_test.go index b502921636..03a5756254 100644 --- a/pkg/operator/encryption/kms/pluginlifecycle/vault_test.go +++ b/pkg/operator/encryption/kms/pluginlifecycle/vault_test.go @@ -29,6 +29,7 @@ func TestVaultSidecarProvider_BuildSidecarContainer(t *testing.T) { containerName string keyID string udsPath string + unsupportedConfig []byte inputContainers []corev1.Container expectedContainers []corev1.Container expectErr string @@ -196,6 +197,98 @@ func TestVaultSidecarProvider_BuildSidecarContainer(t *testing.T) { udsPath: "unix:///var/run/kmsplugin/kms-555.sock", expectErr: "vault AppRole authentication secret name cannot be empty", }, + { + name: "nil unsupported config omits log level", + vaultConfig: configv1.VaultKMSPluginConfig{ + KMSPluginImage: "quay.io/test/vault:v2", + VaultAddress: "https://vault.example.com:8200", + TransitKey: "my-key", + TransitMount: "transit", + Authentication: configv1.VaultAuthentication{ + AppRole: configv1.VaultAppRoleAuthentication{ + Secret: configv1.VaultSecretReference{Name: "vault-approle"}, + }, + }, + }, + secretData: newVaultAppRoleSecretData(t, "test-role-id", "test-secret-id"), + credentialsDir: "/var/run/secrets/kms-plugin", + containerName: "kms-plugin", + keyID: "555", + udsPath: "unix:///var/run/kmsplugin/kms-555.sock", + unsupportedConfig: nil, + expectedContainers: []corev1.Container{ + { + Name: "kms-plugin-555", + Image: "quay.io/test/vault:v2", + Args: []string{ + "-listen-address=unix:///var/run/kmsplugin/kms-555.sock", + "-vault-address=https://vault.example.com:8200", + "-transit-mount=transit", + "-transit-key=my-key", + "-approle-role-id=test-role-id", + "-approle-secret-id-path=/var/run/secrets/kms-plugin/kms-plugin-secret-vault-approle_secret-id-555", + "-tls-skip-verify=true", + "-metrics-port=0", + }, + ImagePullPolicy: corev1.PullIfNotPresent, + RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways), + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("64Mi"), + corev1.ResourceCPU: resource.MustParse("10m"), + }, + }, + }, + }, + }, + { + name: "log level override appended to args", + vaultConfig: configv1.VaultKMSPluginConfig{ + KMSPluginImage: "quay.io/test/vault:v2", + VaultAddress: "https://vault.example.com:8200", + TransitKey: "my-key", + TransitMount: "transit", + Authentication: configv1.VaultAuthentication{ + AppRole: configv1.VaultAppRoleAuthentication{ + Secret: configv1.VaultSecretReference{Name: "vault-approle"}, + }, + }, + }, + secretData: newVaultAppRoleSecretData(t, "test-role-id", "test-secret-id"), + credentialsDir: "/var/run/secrets/kms-plugin", + containerName: "kms-plugin", + keyID: "555", + udsPath: "unix:///var/run/kmsplugin/kms-555.sock", + unsupportedConfig: []byte(`{"encryption":{"kms":{"vault":{"logLevel":"debug-extended"}}}}`), + inputContainers: nil, + expectedContainers: []corev1.Container{ + { + Name: "kms-plugin-555", + Image: "quay.io/test/vault:v2", + Args: []string{ + "-listen-address=unix:///var/run/kmsplugin/kms-555.sock", + "-vault-address=https://vault.example.com:8200", + "-transit-mount=transit", + "-transit-key=my-key", + "-approle-role-id=test-role-id", + "-approle-secret-id-path=/var/run/secrets/kms-plugin/kms-plugin-secret-vault-approle_secret-id-555", + "-log-level=debug-extended", + "-tls-skip-verify=true", + "-metrics-port=0", + }, + ImagePullPolicy: corev1.PullIfNotPresent, + RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways), + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("64Mi"), + corev1.ResourceCPU: resource.MustParse("10m"), + }, + }, + }, + }, + }, } for _, tt := range tests { @@ -212,7 +305,7 @@ func TestVaultSidecarProvider_BuildSidecarContainer(t *testing.T) { keyID: tt.keyID, } - provider, err := newVaultSidecarProvider(tt.containerName, tt.keyID, tt.udsPath, tt.vaultConfig, creds) + provider, err := newVaultSidecarProvider(tt.containerName, tt.keyID, tt.udsPath, tt.vaultConfig, creds, tt.unsupportedConfig) if tt.expectErr != "" { require.EqualError(t, err, tt.expectErr) return