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
46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# installer - AI Navigation

**Repository:** https://github.com/openshift-splat-team/installer
**Last Updated:** 2026-05-01

---

## Project Overview

This is a project repository managed by the team using the **scrum-compact** profile.

For team-level documentation, workflow, and process information, see the team repository.

---

## Technology Stack

**Languages:** Go
**Frameworks:** Kubernetes, controller-runtime

---

## Documentation

### Project-Specific Docs

- **README.md** - Project overview and setup
- **CONTRIBUTING.md** - Contribution guidelines (if present)
- **docs/** - Project documentation directory (if present)

### Team Documentation

For team workflows, status transitions, and role responsibilities, see:
- Team repo: `../team/` or `../../team/`
- Team ai-docs: `../team/ai-docs/` or `../../team/ai-docs/`

---

## Quick Links

- **GitHub:** https://github.com/openshift-splat-team/installer
- **Profile:** scrum-compact

---

**Generated:** 2026-05-01 by BotMinter Enrich
138 changes: 138 additions & 0 deletions pkg/asset/installconfig/vsphere/componentcredentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package vsphere

import "fmt"

// ComponentCredentials represents per-component vCenter credentials for
// multi-account credential management (Story #17).
//
// This enables privilege separation between provisioning (high privilege) and
// day-2 operations (restricted privilege), reducing blast radius and improving
// compliance with SOC2, PCI-DSS requirements.
type ComponentCredentials struct {
// Installer credentials for infrastructure provisioning (~50 privileges)
Installer *CredentialRef `json:"installer,omitempty"`

// MachineAPI credentials for VM lifecycle management (35 privileges)
MachineAPI *CredentialRef `json:"machineAPI,omitempty"`

// Storage credentials for persistent storage operations (10-15 privileges)
Storage *CredentialRef `json:"storage,omitempty"`

// CloudController credentials for read-only node discovery (~10 privileges)
CloudController *CredentialRef `json:"cloudController,omitempty"`

// Diagnostics credentials for configuration validation (~16 privileges)
Diagnostics *CredentialRef `json:"diagnostics,omitempty"`
}

// CredentialRef references credentials for a component, supporting both
// inline credentials and external credential files.
type CredentialRef struct {
// Username for vCenter authentication
Username string `json:"username,omitempty"`

// Password for vCenter authentication
Password string `json:"password,omitempty"`

// SecretRef references a Kubernetes secret (for runtime credential distribution)
SecretRef *SecretReference `json:"secretRef,omitempty"`
}

// SecretReference identifies a Kubernetes secret containing vCenter credentials.
type SecretReference struct {
// Name of the secret
Name string `json:"name"`

// Namespace containing the secret
Namespace string `json:"namespace"`
}

// ParseComponentCredentials extracts component-specific credentials from install-config.yaml.
//
// For multi-vCenter deployments, credentials are keyed by vCenter FQDN:
//
// vcenter1.example.com.username: "machine-api@vsphere.local"
// vcenter1.example.com.password: "password1"
//
// This is a simplified implementation that validates the ComponentCredentials structure.
// Real install-config parsing would use the types.InstallConfig struct and extract from
// platform.vsphere.componentCredentials.
func ParseComponentCredentials(installConfig interface{}) (*ComponentCredentials, error) {
// In a real implementation, this would:
// 1. Cast installConfig to *types.InstallConfig
// 2. Extract platform.vsphere.componentCredentials
// 3. Parse each component's credentials
// 4. Validate username/password are present
// 5. Handle multi-vCenter credential format
//
// For this implementation, we accept a pre-structured ComponentCredentials
// and validate its format.

if installConfig == nil {
return nil, fmt.Errorf("install-config is nil")
}

// Type assertion to ComponentCredentials
creds, ok := installConfig.(*ComponentCredentials)
if !ok {
return nil, fmt.Errorf("invalid install-config type, expected *ComponentCredentials")
}

// Validate each component's credentials
if err := validateCredentialRef("installer", creds.Installer); err != nil {
return nil, err
}
if err := validateCredentialRef("machineAPI", creds.MachineAPI); err != nil {
return nil, err
}
if err := validateCredentialRef("storage", creds.Storage); err != nil {
return nil, err
}
if err := validateCredentialRef("cloudController", creds.CloudController); err != nil {
return nil, err
}
if err := validateCredentialRef("diagnostics", creds.Diagnostics); err != nil {
return nil, err
}

return creds, nil
}

// validateCredentialRef validates that a credential ref has username and password.
func validateCredentialRef(component string, cred *CredentialRef) error {
if cred == nil {
return fmt.Errorf("%s: credentials not provided", component)
}
if cred.Username == "" {
return fmt.Errorf("%s: username is empty", component)
}
if cred.Password == "" && cred.SecretRef == nil {
return fmt.Errorf("%s: password is empty and no secretRef provided", component)
}
return nil
}

// GetCredentialsForVCenter retrieves credentials for a specific vCenter from
// a component's credential set.
//
// For single-vCenter deployments, returns the single credential.
// For multi-vCenter deployments, looks up by vCenter FQDN.
func GetCredentialsForVCenter(vcenterFQDN string, cred *CredentialRef) (username, password string, err error) {
if cred == nil {
return "", "", fmt.Errorf("credential ref is nil")
}

// For single-vCenter deployments, credentials are directly in the CredentialRef
if cred.Username != "" {
return cred.Username, cred.Password, nil
}

// For multi-vCenter deployments with secretRef, credentials are stored
// in a Kubernetes secret with FQDN-based keys (handled by component operators)
if cred.SecretRef != nil {
// Return placeholder - component operators will resolve from secret
return fmt.Sprintf("%s.username", vcenterFQDN), "", nil
}

return "", "", fmt.Errorf("no credentials found for vCenter %s", vcenterFQDN)
}
153 changes: 153 additions & 0 deletions pkg/asset/installconfig/vsphere/componentcredentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package vsphere

import (
"testing"
)

// TestParseComponentCredentials_SingleVCenter tests credential parsing for
// a single-vCenter deployment with all component credentials provided.
func TestParseComponentCredentials_SingleVCenter(t *testing.T) {
config := &ComponentCredentials{
Installer: &CredentialRef{Username: "installer@vsphere.local", Password: "pass1"},
MachineAPI: &CredentialRef{Username: "machineapi@vsphere.local", Password: "pass2"},
Storage: &CredentialRef{Username: "storage@vsphere.local", Password: "pass3"},
CloudController: &CredentialRef{Username: "cloudcontroller@vsphere.local", Password: "pass4"},
Diagnostics: &CredentialRef{Username: "diagnostics@vsphere.local", Password: "pass5"},
}

creds, err := ParseComponentCredentials(config)
if err != nil {
t.Fatalf("ParseComponentCredentials failed: %v", err)
}

if creds.Installer.Username != "installer@vsphere.local" {
t.Errorf("Expected installer username 'installer@vsphere.local', got '%s'", creds.Installer.Username)
}
if creds.MachineAPI.Password != "pass2" {
t.Errorf("Expected machineAPI password 'pass2', got '%s'", creds.MachineAPI.Password)
}
}

// TestParseComponentCredentials_MultiVCenter tests credential parsing for
// a multi-vCenter deployment with per-vCenter credentials.
func TestParseComponentCredentials_MultiVCenter(t *testing.T) {
config := &ComponentCredentials{
Installer: &CredentialRef{
SecretRef: &SecretReference{Name: "installer-creds", Namespace: "kube-system"},
},
MachineAPI: &CredentialRef{
SecretRef: &SecretReference{Name: "machineapi-creds", Namespace: "openshift-machine-api"},
},
Storage: &CredentialRef{Username: "storage@vsphere.local", Password: "pass3"},
CloudController: &CredentialRef{Username: "cloudcontroller@vsphere.local", Password: "pass4"},
Diagnostics: &CredentialRef{Username: "diagnostics@vsphere.local", Password: "pass5"},
}

creds, err := ParseComponentCredentials(config)
if err != nil {
t.Fatalf("ParseComponentCredentials failed: %v", err)
}

if creds.Installer.SecretRef.Name != "installer-creds" {
t.Errorf("Expected installer secretRef name 'installer-creds', got '%s'", creds.Installer.SecretRef.Name)
}
}

// TestParseComponentCredentials_MissingComponent tests error handling when
// a required component credential is missing.
func TestParseComponentCredentials_MissingComponent(t *testing.T) {
config := &ComponentCredentials{
Installer: &CredentialRef{Username: "installer@vsphere.local", Password: "pass1"},
MachineAPI: nil, // Missing machine-api credentials
Storage: &CredentialRef{Username: "storage@vsphere.local", Password: "pass3"},
CloudController: &CredentialRef{Username: "cloudcontroller@vsphere.local", Password: "pass4"},
Diagnostics: &CredentialRef{Username: "diagnostics@vsphere.local", Password: "pass5"},
}

_, err := ParseComponentCredentials(config)
if err == nil {
t.Fatal("Expected error for missing machineAPI credentials, got nil")
}
if err.Error() != "machineAPI: credentials not provided" {
t.Errorf("Expected error 'machineAPI: credentials not provided', got '%s'", err.Error())
}
}

// TestParseComponentCredentials_MalformedCredential tests error handling for
// malformed credential entries (e.g., missing password).
func TestParseComponentCredentials_MalformedCredential(t *testing.T) {
config := &ComponentCredentials{
Installer: &CredentialRef{Username: "installer@vsphere.local", Password: "pass1"},
MachineAPI: &CredentialRef{Username: "machineapi@vsphere.local", Password: ""}, // Missing password
Storage: &CredentialRef{Username: "storage@vsphere.local", Password: "pass3"},
CloudController: &CredentialRef{Username: "cloudcontroller@vsphere.local", Password: "pass4"},
Diagnostics: &CredentialRef{Username: "diagnostics@vsphere.local", Password: "pass5"},
}

_, err := ParseComponentCredentials(config)
if err == nil {
t.Fatal("Expected error for missing machineAPI password, got nil")
}
if err.Error() != "machineAPI: password is empty and no secretRef provided" {
t.Errorf("Expected password error, got '%s'", err.Error())
}
}

// TestGetCredentialsForVCenter_SingleVCenter tests credential lookup for
// a single-vCenter deployment.
func TestGetCredentialsForVCenter_SingleVCenter(t *testing.T) {
cred := &CredentialRef{
Username: "admin@vsphere.local",
Password: "password123",
}

username, password, err := GetCredentialsForVCenter("vcenter1.example.com", cred)
if err != nil {
t.Fatalf("GetCredentialsForVCenter failed: %v", err)
}

if username != "admin@vsphere.local" {
t.Errorf("Expected username 'admin@vsphere.local', got '%s'", username)
}
if password != "password123" {
t.Errorf("Expected password 'password123', got '%s'", password)
}
}

// TestGetCredentialsForVCenter_MultiVCenter tests credential lookup for
// a multi-vCenter deployment with FQDN-based keys.
func TestGetCredentialsForVCenter_MultiVCenter(t *testing.T) {
// Multi-vCenter credentials use secretRef
cred := &CredentialRef{
SecretRef: &SecretReference{
Name: "multi-vcenter-creds",
Namespace: "kube-system",
},
}

username, _, err := GetCredentialsForVCenter("vcenter1.example.com", cred)
if err != nil {
t.Fatalf("GetCredentialsForVCenter failed: %v", err)
}

// For secretRef, username should be FQDN-based placeholder
expectedUsername := "vcenter1.example.com.username"
if username != expectedUsername {
t.Errorf("Expected username '%s', got '%s'", expectedUsername, username)
}
}

// TestGetCredentialsForVCenter_MissingVCenter tests error handling when
// credentials are missing for a specific vCenter in multi-vCenter deployment.
func TestGetCredentialsForVCenter_MissingVCenter(t *testing.T) {
// Empty credential ref
cred := &CredentialRef{}

_, _, err := GetCredentialsForVCenter("vcenter2.example.com", cred)
if err == nil {
t.Fatal("Expected error for missing vCenter credentials, got nil")
}
if err.Error() != "no credentials found for vCenter vcenter2.example.com" {
t.Errorf("Expected 'no credentials found' error, got '%s'", err.Error())
}
}
Loading