Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkg/operator/encryption/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,14 @@ func NewControllers(
provider,
encryptionEnabledChecker.PreconditionFulfilled,
preflightDeployer,
deployer,
operatorClient,
apiServerClient,
apiServerInformer,
kubeInformersForNamespaces,
secretsClient,
configMapClient,
encryptionSecretSelector,
encryptionStatusProvider,
eventRecorder,
))
Expand Down
204 changes: 204 additions & 0 deletions pkg/operator/encryption/controllers/encryption_key_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package controllers

import (
"context"
"encoding/base64"
"fmt"
"sort"
"strings"

configv1 "github.com/openshift/api/config/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"

"github.com/openshift/library-go/pkg/operator/encryption/crypto"
"github.com/openshift/library-go/pkg/operator/encryption/secrets"
"github.com/openshift/library-go/pkg/operator/encryption/state"
)

type encryptionKeyPlan struct {
needed bool
keyID uint64
reasons []string
internalReason string
}

func planNextEncryptionKey(
desiredEncryptionState map[schema.GroupResource]state.GroupResourceState,
currentMode state.Mode,
externalReason string,
encryptedGRs []schema.GroupResource,
desiredProviderCfg kmsProviderConfig,
) (*encryptionKeyPlan, error) {
plan := &encryptionKeyPlan{}
reasons := []string{}

var (
commonReason string
hasCommonReason bool
commonReasonDiffers bool
)

for gr, grKeys := range desiredEncryptionState {
latestKeyID, internalReason, needed, err := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs, desiredProviderCfg)
if err != nil {
return nil, err
}
if !needed {
continue
}

if !hasCommonReason {
commonReason = internalReason
hasCommonReason = true
} else if commonReason != internalReason {
commonReasonDiffers = true
}

plan.needed = true
nextKeyID := latestKeyID + 1
if plan.keyID < nextKeyID {
plan.keyID = nextKeyID
}
reasons = append(reasons, fmt.Sprintf("%s-%s", gr.Resource, internalReason))
}

if !plan.needed {
return plan, nil
}
if hasCommonReason && !commonReasonDiffers && len(reasons) > 1 {
reasons = []string{commonReason}
}

sort.Strings(reasons)
plan.reasons = reasons
plan.internalReason = strings.Join(reasons, ", ")
return plan, nil
}

// encryptionKeyBuildResult is the in-memory key material plus any referenced
// Secret/ConfigMap fetched while building it. Callers that also need to hash
// the KMS config (key controller) reuse the refs to avoid a second API round-trip.
type encryptionKeyBuildResult struct {
keyState state.KeyState
refSecret *corev1.Secret
refCM *corev1.ConfigMap
}

func buildEncryptionKeyState(
ctx context.Context,
keyID uint64,
currentMode state.Mode,
apiServerEncryption configv1.APIServerEncryption,
desiredProviderCfg kmsProviderConfig,
secretClient corev1client.SecretsGetter,
configMapClient corev1client.ConfigMapsGetter,
internalReason string,
externalReason string,
kmsEndpointOverride string,
) (*encryptionKeyBuildResult, error) {
bs := crypto.ModeToNewKeyFunc[currentMode]()
result := &encryptionKeyBuildResult{
keyState: state.KeyState{
Key: apiserverv1.Key{
Name: fmt.Sprintf("%d", keyID),
Secret: base64.StdEncoding.EncodeToString(bs),
},
Mode: currentMode,
InternalReason: internalReason,
ExternalReason: externalReason,
},
}

if currentMode != state.KMS {
return result, nil
}

endpoint := kmsEndpointOverride
if len(endpoint) == 0 {
endpoint = fmt.Sprintf(kmsEndpointFormat, keyID)
}
result.keyState.KMS = &state.KMSState{
Encryption: &apiserverv1.KMSConfiguration{
APIVersion: "v2",
Name: fmt.Sprintf("%d", keyID),
Endpoint: endpoint,
Timeout: &metav1.Duration{Duration: defaultKMSTimeout},
},
Plugin: apiServerEncryption.KMS,
}

if secretName, expectedKeys, err := desiredProviderCfg.referencedSecretName(); err != nil {
return nil, err
} else if len(secretName) > 0 {
refSecret, err := secretClient.Secrets(openshiftConfigNS).Get(ctx, secretName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get secret %s in %s: %w", secretName, openshiftConfigNS, err)
}
result.refSecret = refSecret
for _, key := range expectedKeys {
v, ok := refSecret.Data[key]
if !ok {
return nil, fmt.Errorf("secret %s in %s is missing required key %q", secretName, openshiftConfigNS, key)
}
if err := result.keyState.KMS.PluginSecretData.Set(secretName, key, v); err != nil {
return nil, err
}
}
}

if cmName, expectedKeys, err := desiredProviderCfg.referencedConfigMapName(); err != nil {
return nil, err
} else if len(cmName) > 0 {
refCM, err := configMapClient.ConfigMaps(openshiftConfigNS).Get(ctx, cmName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get configmap %s in %s: %w", cmName, openshiftConfigNS, err)
}
result.refCM = refCM
for _, key := range expectedKeys {
v, ok := refCM.Data[key]
if !ok {
return nil, fmt.Errorf("configmap %s in %s is missing required key %q", cmName, openshiftConfigNS, key)
}
if err := result.keyState.KMS.PluginConfigMapData.Set(cmName, key, []byte(v)); err != nil {
return nil, err
}
}
}
Comment on lines +134 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add context to bubbled-up errors.

Errors from desiredProviderCfg.referencedSecretName()/referencedConfigMapName() (Lines 123-125, 141-143) and from PluginSecretData.Set/PluginConfigMapData.Set (Lines 135-137, 153-155) are returned bare, unlike the sibling Get() calls a few lines above/below that wrap errors with fmt.Errorf("failed to get secret %s in %s: %w", ...). This makes failures in these paths harder to diagnose in logs.

🐛 Proposed fix
-	if secretName, expectedKeys, err := desiredProviderCfg.referencedSecretName(); err != nil {
-		return state.KeyState{}, err
+	if secretName, expectedKeys, err := desiredProviderCfg.referencedSecretName(); err != nil {
+		return state.KeyState{}, fmt.Errorf("failed to resolve referenced secret name: %w", err)
	} else if len(secretName) > 0 {
		...
-			if err := ks.KMS.PluginSecretData.Set(secretName, key, v); err != nil {
-				return state.KeyState{}, err
+			if err := ks.KMS.PluginSecretData.Set(secretName, key, v); err != nil {
+				return state.KeyState{}, fmt.Errorf("failed to set plugin secret data %s/%s: %w", secretName, key, err)
			}

(similarly for the configmap branch)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/operator/encryption/controllers/encryption_key_helpers.go` around lines
123 - 157, Wrap errors returned by desiredProviderCfg.referencedSecretName() and
referencedConfigMapName() with context identifying the referenced secret or
ConfigMap resolution, and wrap failures from ks.KMS.PluginSecretData.Set and
PluginConfigMapData.Set with the corresponding resource name and key. Preserve
the existing error propagation while using %w so callers retain the underlying
errors.


return result, nil
}

func buildEncryptionKeySecret(
ctx context.Context,
instanceName string,
keyID uint64,
currentMode state.Mode,
apiServerEncryption configv1.APIServerEncryption,
desiredProviderCfg kmsProviderConfig,
secretClient corev1client.SecretsGetter,
configMapClient corev1client.ConfigMapsGetter,
internalReason string,
externalReason string,
kmsEndpointOverride string,
) (*corev1.Secret, error) {
result, err := buildEncryptionKeyState(
ctx,
keyID,
currentMode,
apiServerEncryption,
desiredProviderCfg,
secretClient,
configMapClient,
internalReason,
externalReason,
kmsEndpointOverride,
)
if err != nil {
return nil, err
}
return secrets.FromKeyState(instanceName, result.keyState)
}
99 changes: 99 additions & 0 deletions pkg/operator/encryption/controllers/encryption_key_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package controllers

import (
"context"
"strings"
"testing"

configv1 "github.com/openshift/api/config/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"

"github.com/openshift/library-go/pkg/operator/encryption/state"
)

func TestBuildEncryptionKeyStateMissingRefs(t *testing.T) {
apiServerEncryption := configv1.APIServerEncryption{
Type: configv1.EncryptionTypeKMS,
KMS: configv1.KMSPluginConfig{
Type: configv1.VaultKMSProvider,
Vault: wellKnownBaseVaultConfig,
},
}
providerCfg, err := newKMSProviderConfig(apiServerEncryption.KMS)
if err != nil {
t.Fatalf("failed to create provider config: %v", err)
}

t.Run("missing referenced secret", func(t *testing.T) {
client := fake.NewSimpleClientset(&wellKnownBaseConfigMap)
_, err := buildEncryptionKeyState(
context.TODO(),
1,
state.KMS,
apiServerEncryption,
providerCfg,
client.CoreV1(),
client.CoreV1(),
"test-reason",
"",
"",
)
if err == nil {
t.Fatal("expected an error")
}
if !strings.Contains(err.Error(), "vault-approle") {
t.Fatalf("expected error mentioning the missing secret, got: %v", err)
}
})

t.Run("missing referenced configmap", func(t *testing.T) {
client := fake.NewSimpleClientset(&wellKnownBaseSecret)
_, err := buildEncryptionKeyState(
context.TODO(),
1,
state.KMS,
apiServerEncryption,
providerCfg,
client.CoreV1(),
client.CoreV1(),
"test-reason",
"",
"",
)
if err == nil {
t.Fatal("expected an error")
}
if !strings.Contains(err.Error(), "vault-ca-bundle") {
t.Fatalf("expected error mentioning the missing configmap, got: %v", err)
}
})

t.Run("returns fetched refs for hasher reuse", func(t *testing.T) {
client := fake.NewSimpleClientset([]runtime.Object{&wellKnownBaseSecret, &wellKnownBaseConfigMap}...)
result, err := buildEncryptionKeyState(
context.TODO(),
1,
state.KMS,
apiServerEncryption,
providerCfg,
client.CoreV1(),
client.CoreV1(),
"test-reason",
"",
"unix:///var/run/kmsplugin/kms.sock",
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.refSecret == nil || result.refSecret.Name != "vault-approle" {
t.Fatalf("expected refSecret vault-approle, got %+v", result.refSecret)
}
if result.refCM == nil || result.refCM.Name != "vault-ca-bundle" {
t.Fatalf("expected refCM vault-ca-bundle, got %+v", result.refCM)
}
if result.keyState.KMS == nil || result.keyState.KMS.Encryption.Endpoint != "unix:///var/run/kmsplugin/kms.sock" {
t.Fatalf("expected endpoint override, got %+v", result.keyState.KMS)
}
})
}
Loading