Skip to content
Open
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
66 changes: 66 additions & 0 deletions pkg/vsphere/actuator/credential_distribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package actuator

import (
"context"
"fmt"

minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
corev1 "k8s.io/api/core/v1"
)

const vsphereComponentAnnotationKey = "cloudcredential.openshift.io/vsphere-component"

// ComponentSecretReader looks up a named secret from kube-system.
type ComponentSecretReader interface {
GetComponentSecret(ctx context.Context, name string) (*corev1.Secret, error)
}

// componentSecretName maps an annotation value to the kube-system secret name.
// Returns "" for unrecognized components.
func componentSecretName(component string) string {
switch component {
case "machineAPI":
return "vsphere-machine-api-creds"
case "csiDriver":
return "vsphere-storage-creds"
case "cloudController":
return "vsphere-cloud-controller-creds"
case "vsphereProblemDetector":
return "vsphere-problem-detector-creds"
default:
return ""
}
}

// resolveVSphereCredentials routes a CredentialsRequest to per-component or shared credentials.
//
// If the CR has a vsphere-component annotation and a matching kube-system secret with non-empty
// data, the per-component credential is returned. If the component secret is absent or empty,
// shared credentials are returned and a warning is set. A CR without the annotation is a
// passthrough — shared credentials, no warning.
func resolveVSphereCredentials(
ctx context.Context,
cr *minterv1.CredentialsRequest,
componentSecretReader ComponentSecretReader,
sharedSecretData map[string][]byte,
) (secretData map[string][]byte, warning string, err error) {
component := cr.Annotations[vsphereComponentAnnotationKey]
if component == "" {
return sharedSecretData, "", nil
}

secretName := componentSecretName(component)
if secretName != "" {
secret, err := componentSecretReader.GetComponentSecret(ctx, secretName)
if err != nil {
return nil, "", err
}

if secret != nil && len(secret.Data) > 0 {
return secret.Data, "", nil
}
Comment on lines +41 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Missing hard validation for multi-vCenter credential completeness.

At Line 59, any non-empty component secret is accepted. That allows partial credentials (for example only one vCenter, or missing .password) to pass, which breaks the expected “error on missing vCenter entry” behavior.

Proposed direction
 func resolveVSphereCredentials(
 	ctx context.Context,
 	cr *minterv1.CredentialsRequest,
 	componentSecretReader ComponentSecretReader,
 	sharedSecretData map[string][]byte,
+	requiredVCenters []string,
 ) (secretData map[string][]byte, warning string, err error) {
@@
-		if secret != nil && len(secret.Data) > 0 {
-			return secret.Data, "", nil
+		if secret != nil && len(secret.Data) > 0 {
+			for _, vc := range requiredVCenters {
+				userKey := vc + ".username"
+				passKey := vc + ".password"
+				if len(secret.Data[userKey]) == 0 || len(secret.Data[passKey]) == 0 {
+					return nil, "", fmt.Errorf("missing component credential entries for vCenter %q", vc)
+				}
+			}
+			return secret.Data, "", nil
 		}
 	}
🤖 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/vsphere/actuator/credential_distribution.go` around lines 41 - 61,
resolveVSphereCredentials currently accepts any non-empty component secret (from
componentSecretReader.GetComponentSecret) which lets partial vCenter credentials
pass; update the function to validate completeness of multi-vCenter credentials
before returning secret.Data by checking that for every vCenter entry the
required fields (e.g., the corresponding .username and .password keys or the
expected credential structure) exist and are non-empty; if any vCenter entry or
required key (such as password) is missing, return a descriptive error (rather
than nil) so callers can surface the missing-vCenter entry behavior; use the
componentSecretName and vsphereComponentAnnotationKey to locate the secret and
validate secret.Data and, similarly, validate sharedSecretData if used as a
fallback.

}

// No component secret (or unrecognized component): fall back to shared credentials.
return sharedSecretData, fmt.Sprintf("Component '%s' using fallback shared credentials", component), nil
}
259 changes: 259 additions & 0 deletions pkg/vsphere/actuator/credential_distribution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// credential_distribution_test.go tests Story #38:
// CCO Per-Component Credential Distribution with Graceful Fallback.
//
// Implementation contract expected by these tests:
//
// func resolveVSphereCredentials(
// ctx context.Context,
// cr *minterv1.CredentialsRequest,
// componentSecretReader ComponentSecretReader,
// sharedSecretData map[string][]byte,
// ) (secretData map[string][]byte, warning string, err error)
//
// ComponentSecretReader is an interface (or func) that looks up a named secret from kube-system.
// The component annotation key is: cloudcredential.openshift.io/vsphere-component
// Annotation values: machineAPI, csiDriver, cloudController, vsphereProblemDetector
// Component secret names: vsphere-machine-api-creds, vsphere-storage-creds,
//
// vsphere-cloud-controller-creds, vsphere-problem-detector-creds
//
// Per-component secret key format: {vcenter.fqdn}.username / {vcenter.fqdn}.password
// Warning format: Component '{component}' using fallback shared credentials
package actuator

import (
"context"
"testing"

minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// vsphereComponentAnnotation is the annotation key used to identify per-component CredentialsRequests.
const vsphereComponentAnnotation = "cloudcredential.openshift.io/vsphere-component"

// stubComponentSecretReader is a test double for looking up kube-system component secrets.
type stubComponentSecretReader struct {
secrets map[string]*corev1.Secret // secretName → secret (nil means NotFound)
}

func (s *stubComponentSecretReader) GetComponentSecret(_ context.Context, name string) (*corev1.Secret, error) {
if secret, ok := s.secrets[name]; ok {
return secret, nil // nil value means secret was explicitly absent
}
return nil, nil // not configured = absent
}

// makeCredCR builds a minimal CredentialsRequest with the given component annotation value.
func makeCredCR(component string) *minterv1.CredentialsRequest {
cr := &minterv1.CredentialsRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cr",
Namespace: "openshift-cloud-credential-operator",
},
}
if component != "" {
cr.Annotations = map[string]string{vsphereComponentAnnotation: component}
}
return cr
}

// makeComponentSecret builds a kube-system Secret with the given vCenter credentials.
func makeComponentSecret(name string, creds map[string]string) *corev1.Secret {
data := make(map[string][]byte, len(creds))
for k, v := range creds {
data[k] = []byte(v)
}
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "kube-system"},
Data: data,
}
}

var sharedSecretData = map[string][]byte{
"vcenter.example.com.username": []byte("shared@vsphere.local"),
"vcenter.example.com.password": []byte("shared-password"),
}

// ── AC1: Per-component routing and shared-credential fallback ─────────────────

// TestResolveComponentCredential_AnnotatedCRWithSecret_UsesPerComponentCred verifies AC1 (first
// branch): a CredentialsRequest annotated machineAPI, whose component secret exists, receives the
// per-component credential — not the shared credential.
func TestResolveComponentCredential_AnnotatedCRWithSecret_UsesPerComponentCred(t *testing.T) {
cr := makeCredCR("machineAPI")

componentSecret := makeComponentSecret("vsphere-machine-api-creds", map[string]string{
"vcenter.example.com.username": "machine-api@vsphere.local",
"vcenter.example.com.password": "machine-api-password",
})

reader := &stubComponentSecretReader{
secrets: map[string]*corev1.Secret{
"vsphere-machine-api-creds": componentSecret,
},
}

data, warning, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if warning != "" {
t.Errorf("expected no warning for annotated CR with existing component secret, got: %q", warning)
}
if string(data["vcenter.example.com.username"]) != "machine-api@vsphere.local" {
t.Errorf("expected per-component username, got: %q", data["vcenter.example.com.username"])
}
}

// TestResolveComponentCredential_AnnotatedCRWithoutSecret_FallsBackToShared verifies AC1 (fallback
// branch): a CredentialsRequest annotated csiDriver, with no component secret present, receives the
// shared credential and a warning is returned.
func TestResolveComponentCredential_AnnotatedCRWithoutSecret_FallsBackToShared(t *testing.T) {
cr := makeCredCR("csiDriver")

// No component secret registered for csiDriver
reader := &stubComponentSecretReader{secrets: map[string]*corev1.Secret{}}

data, warning, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("expected no error during fallback, got: %v", err)
}
expectedWarning := "Component 'csiDriver' using fallback shared credentials"
if warning != expectedWarning {
t.Errorf("expected warning %q, got %q", expectedWarning, warning)
}
if string(data["vcenter.example.com.username"]) != "shared@vsphere.local" {
t.Errorf("expected shared username in fallback data, got: %q", data["vcenter.example.com.username"])
}
}

// ── AC2: Multi-vCenter credential distribution ────────────────────────────────

// TestResolveComponentCredential_MultiVCenter_BothVCentersPopulated verifies AC2: when a
// component secret contains credentials for two vCenters, both sets of keyed entries appear
// in the provisioned secret data.
func TestResolveComponentCredential_MultiVCenter_BothVCentersPopulated(t *testing.T) {
cr := makeCredCR("machineAPI")

componentSecret := makeComponentSecret("vsphere-machine-api-creds", map[string]string{
"vcenter1.example.com.username": "machine-api@vc1.local",
"vcenter1.example.com.password": "password1",
"vcenter2.example.com.username": "machine-api@vc2.local",
"vcenter2.example.com.password": "password2",
})

reader := &stubComponentSecretReader{
secrets: map[string]*corev1.Secret{"vsphere-machine-api-creds": componentSecret},
}

data, _, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if string(data["vcenter1.example.com.username"]) != "machine-api@vc1.local" {
t.Errorf("vcenter1 username missing or wrong: %q", data["vcenter1.example.com.username"])
}
if string(data["vcenter2.example.com.username"]) != "machine-api@vc2.local" {
t.Errorf("vcenter2 username missing or wrong: %q", data["vcenter2.example.com.username"])
}
}

// ── Adversarial cases ─────────────────────────────────────────────────────────

// TestResolveComponentCredential_NoAnnotation_Passthrough verifies that a CredentialsRequest
// with no vsphere-component annotation is not routed to per-component logic (passthrough).
func TestResolveComponentCredential_NoAnnotation_Passthrough(t *testing.T) {
cr := makeCredCR("") // no annotation

reader := &stubComponentSecretReader{secrets: map[string]*corev1.Secret{}}

data, warning, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("expected no error for passthrough CR, got: %v", err)
}
if warning != "" {
t.Errorf("expected no warning for passthrough CR, got: %q", warning)
}
// Passthrough returns the shared secret data unchanged
if string(data["vcenter.example.com.username"]) != "shared@vsphere.local" {
t.Errorf("expected shared credential in passthrough, got: %q", data["vcenter.example.com.username"])
}
}

// TestResolveComponentCredential_NilAnnotations_Passthrough is an edge case ensuring nil
// annotations map (not just absent key) is handled without panic.
func TestResolveComponentCredential_NilAnnotations_Passthrough(t *testing.T) {
cr := &minterv1.CredentialsRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cr",
Namespace: "openshift-cloud-credential-operator",
Annotations: nil, // explicitly nil
},
}

reader := &stubComponentSecretReader{secrets: map[string]*corev1.Secret{}}

_, _, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("nil annotations must not panic or error, got: %v", err)
}
}

// TestResolveComponentCredential_UnknownComponentAnnotation_FallsBackToShared verifies that an
// unrecognized annotation value (e.g. "diagnostics") does not panic and falls back to shared.
func TestResolveComponentCredential_UnknownComponentAnnotation_FallsBackToShared(t *testing.T) {
cr := makeCredCR("diagnostics") // not a known component

reader := &stubComponentSecretReader{secrets: map[string]*corev1.Secret{}}

_, warning, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("unknown component should fall back gracefully, got error: %v", err)
}
if warning == "" {
t.Error("expected a fallback warning for unknown component")
}
}

// TestResolveComponentCredential_EmptyComponentSecret_FallsBackToShared verifies that a component
// secret with no keys (empty Data) is treated equivalently to a missing secret — fallback occurs.
func TestResolveComponentCredential_EmptyComponentSecret_FallsBackToShared(t *testing.T) {
cr := makeCredCR("machineAPI")

emptySecret := makeComponentSecret("vsphere-machine-api-creds", map[string]string{}) // no keys

reader := &stubComponentSecretReader{
secrets: map[string]*corev1.Secret{"vsphere-machine-api-creds": emptySecret},
}

_, warning, err := resolveVSphereCredentials(
context.Background(), cr, reader, sharedSecretData,
)

if err != nil {
t.Fatalf("empty component secret should fall back, not error: %v", err)
}
if warning == "" {
t.Error("expected fallback warning for empty component secret")
}
}