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
125 changes: 125 additions & 0 deletions pkg/asset/cluster/multi_vcenter_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package cluster

import (
"testing"
)

// TestMultiVCenterOperations_MachineAPICreateVM verifies Machine API performs
// VM creation on vcenter1.
//
// Acceptance Criteria: "And components successfully perform operations on their
// respective vCenters"
//
// Test Steps:
// 1. Configure machineAPI → vcenter1.example.com
// 2. Trigger machine creation (scale up machineSet)
// 3. Monitor vCenter API calls
// 4. Verify VM created on vcenter1.example.com datacenter
//
// Expected Result:
// - VM appears in vcenter1.example.com inventory
// - vCenter1 audit log shows machine-api@vsphere.local user
// - vcenter2.example.com has no related activity
func TestMultiVCenterOperations_MachineAPICreateVM(t *testing.T) {
t.Skip("Implementation pending - Story #8")
// TODO: Implement integration test
// 1. Mock vcenter1.example.com and vcenter2.example.com
// 2. Configure machineAPI with vcenter1 credentials
// 3. Trigger machine creation workflow
// 4. Assert VM creation API calls made to vcenter1 only
// 5. Assert VM appears in vcenter1 inventory
// 6. Assert no API calls made to vcenter2
}

// TestMultiVCenterOperations_CSIProvisionPV verifies CSI Driver provisions PVs
// on vcenter2.
//
// Acceptance Criteria: "And components successfully perform operations on their
// respective vCenters"
//
// Test Steps:
// 1. Configure csiDriver → vcenter2.example.com
// 2. Create PersistentVolumeClaim
// 3. Monitor CSI provisioning workflow
// 4. Verify VMDK created on vcenter2.example.com datastore
//
// Expected Result:
// - VMDK appears in vcenter2.example.com datastore
// - vCenter2 audit log shows csi-driver@vsphere.local user
// - vcenter1.example.com has no related activity
func TestMultiVCenterOperations_CSIProvisionPV(t *testing.T) {
t.Skip("Implementation pending - Story #8")
// TODO: Implement integration test
// 1. Mock vcenter1.example.com and vcenter2.example.com
// 2. Configure csiDriver with vcenter2 credentials
// 3. Create PersistentVolumeClaim
// 4. Trigger CSI provisioning workflow
// 5. Assert VMDK creation API calls made to vcenter2 only
// 6. Assert VMDK appears in vcenter2 datastore
// 7. Assert no API calls made to vcenter1
}

// TestMultiVCenterIntegration_FullInstallation verifies complete installation
// flow with multi-vCenter topology.
//
// Acceptance Criteria: End-to-end multi-vCenter installation
//
// Test Steps:
// 1. Configure full multi-vCenter install-config:
// - Platform vCenter: vcenter-default.example.com
// - machineAPI → vcenter1.example.com
// - csiDriver → vcenter2.example.com
// - cloudController → vcenter-default.example.com (no override)
// 2. Run full installation workflow
// 3. Verify all components connect to correct vCenters
// 4. Verify cluster operational
//
// Expected Result:
// - Installation completes successfully
// - Machine API uses vcenter1 for VM operations
// - CSI Driver uses vcenter2 for storage operations
// - Cloud Controller uses vcenter-default for node discovery
// - All components functional and cluster healthy
func TestMultiVCenterIntegration_FullInstallation(t *testing.T) {
t.Skip("Implementation pending - Story #8 - Requires multi-vCenter test environment")
// TODO: Implement E2E integration test
// 1. Setup multi-vCenter test environment (vcenter1, vcenter2, vcenter-default)
// 2. Create install-config with multi-vCenter componentCredentials
// 3. Run openshift-install create cluster
// 4. Wait for installation to complete
// 5. Verify all component secrets have correct FQDN-keyed format
// 6. Scale machineSet and verify VM created on vcenter1
// 7. Create PVC and verify VMDK created on vcenter2
// 8. Verify node discovery uses vcenter-default
// 9. Assert cluster healthy and all components operational
}

// TestMultiVCenterIntegration_AuditTrail verifies vCenter audit logs show
// distinct component usernames.
//
// Acceptance Criteria: "And vCenter event logs show distinct usernames for each
// component's actions"
//
// Test Steps:
// 1. Configure multi-vCenter installation
// 2. Perform operations with each component
// 3. Query vCenter audit logs
// 4. Verify distinct usernames in audit trail
//
// Expected Result:
// - vcenter1 logs show machine-api@vsphere.local for VM operations
// - vcenter2 logs show csi-driver@vsphere.local for storage operations
// - vcenter-default logs show cloud-controller@vsphere.local for node queries
// - Each component's actions clearly attributable to its account
func TestMultiVCenterIntegration_AuditTrail(t *testing.T) {
t.Skip("Implementation pending - Story #8 - Requires multi-vCenter test environment")
// TODO: Implement E2E integration test
// 1. Setup multi-vCenter test environment with audit logging enabled
// 2. Run multi-vCenter installation
// 3. Trigger operations for each component (VM create, PV provision, node sync)
// 4. Query vCenter event logs via vSphere API
// 5. Assert machine-api@vsphere.local appears in vcenter1 logs
// 6. Assert csi-driver@vsphere.local appears in vcenter2 logs
// 7. Assert cloud-controller@vsphere.local appears in vcenter-default logs
// 8. Assert no cross-component username confusion
}
181 changes: 181 additions & 0 deletions pkg/asset/installconfig/vsphere/credentialsfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package vsphere

import (
"fmt"
"os"
"path/filepath"

"gopkg.in/yaml.v3"

"github.com/openshift/installer/pkg/types/vsphere"
)

// CredentialsFileDefaultPath is the default location for the vSphere credentials file.
var CredentialsFileDefaultPath = filepath.Join(os.Getenv("HOME"), ".vsphere", "credentials")

// CredentialsFile represents the YAML structure of the vSphere credentials file.
// The file uses vCenter FQDNs as top-level keys, with component-specific credentials nested underneath.
//
// Example YAML format:
//
// vcenter1.example.com:
// installer:
// username: admin@vsphere.local
// password: <password>
// machine-api:
// username: ocp-machine-api@vsphere.local
// password: <password>
// csi-driver:
// username: ocp-csi@vsphere.local
// password: <password>
// cloud-controller:
// username: ocp-ccm@vsphere.local
// password: <password>
// diagnostics:
// username: ocp-diagnostics@vsphere.local
// password: <password>
type CredentialsFile map[string]VCenterComponentCredentials

// VCenterComponentCredentials holds component credentials for a single vCenter.
type VCenterComponentCredentials struct {
Installer *ComponentAccount `yaml:"installer,omitempty"`
MachineAPI *ComponentAccount `yaml:"machine-api,omitempty"`
CSIDriver *ComponentAccount `yaml:"csi-driver,omitempty"`
CloudController *ComponentAccount `yaml:"cloud-controller,omitempty"`
Diagnostics *ComponentAccount `yaml:"diagnostics,omitempty"`
}

// ComponentAccount holds username and password for a component.
type ComponentAccount struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}

// LoadCredentialsFile reads and parses the vSphere credentials file from the specified path.
// Returns the parsed credentials file or an error if the file cannot be read or parsed.
// If the file does not exist, returns (nil, nil) to indicate graceful fallback.
func LoadCredentialsFile(path string) (*CredentialsFile, error) {
// If path is empty, use default
if path == "" {
path = CredentialsFileDefaultPath
}

// Check if file exists
info, err := os.Stat(path)
if os.IsNotExist(err) {
// File doesn't exist - graceful fallback (not an error)
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to stat credentials file %s: %w", path, err)
}

// Validate file permissions (must be 0600)
if err := validateFilePermissions(path, info); err != nil {
return nil, err
}

// Read file contents
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read credentials file %s: %w", path, err)
}

// Handle empty file (graceful fallback)
if len(data) == 0 {
return nil, nil
}

// Parse YAML
var credsFile CredentialsFile
if err := yaml.Unmarshal(data, &credsFile); err != nil {
return nil, fmt.Errorf("failed to parse YAML credentials file %s: %w", path, err)
}

return &credsFile, nil
}

// validateFilePermissions ensures the credentials file has 0600 permissions (read/write for owner only).
func validateFilePermissions(path string, info os.FileInfo) error {
mode := info.Mode()
perm := mode.Perm()

// File must be 0600 (read/write for owner only)
if perm != 0600 {
return fmt.Errorf("credentials file %s has permissions %04o, must be 0600", path, perm)
}

return nil
}

// MergeWithComponentCredentials merges credentials from the credentials file into the install-config
// ComponentCredentials. Precedence: install-config > credentials file.
// For each component:
// - If install-config has credentials for that component, use install-config (ignore file)
// - If install-config does NOT have credentials, use credentials file (if available)
// - If neither has credentials, component will fall back to legacy passthrough mode
func MergeWithComponentCredentials(
installConfigCreds *vsphere.ComponentCredentials,
credsFile *CredentialsFile,
vCenterFQDN string,
) *vsphere.ComponentCredentials {
// If no credentials file, return install-config credentials as-is
if credsFile == nil {
return installConfigCreds
}

// Get vCenter-specific credentials from file
vcenterCreds, exists := (*credsFile)[vCenterFQDN]
if !exists {
// No credentials for this vCenter in file, return install-config as-is
return installConfigCreds
}

// If no install-config component credentials exist, initialize empty struct
if installConfigCreds == nil {
installConfigCreds = &vsphere.ComponentCredentials{}
}

// Merge each component (install-config takes precedence)
if installConfigCreds.Installer == nil && vcenterCreds.Installer != nil {
installConfigCreds.Installer = &vsphere.AccountCredentials{
Username: vcenterCreds.Installer.Username,
Password: vcenterCreds.Installer.Password,
VCenter: vCenterFQDN,
}
}

if installConfigCreds.MachineAPI == nil && vcenterCreds.MachineAPI != nil {
installConfigCreds.MachineAPI = &vsphere.AccountCredentials{
Username: vcenterCreds.MachineAPI.Username,
Password: vcenterCreds.MachineAPI.Password,
VCenter: vCenterFQDN,
}
}

if installConfigCreds.CSIDriver == nil && vcenterCreds.CSIDriver != nil {
installConfigCreds.CSIDriver = &vsphere.AccountCredentials{
Username: vcenterCreds.CSIDriver.Username,
Password: vcenterCreds.CSIDriver.Password,
VCenter: vCenterFQDN,
}
}

if installConfigCreds.CloudController == nil && vcenterCreds.CloudController != nil {
installConfigCreds.CloudController = &vsphere.AccountCredentials{
Username: vcenterCreds.CloudController.Username,
Password: vcenterCreds.CloudController.Password,
VCenter: vCenterFQDN,
}
}

if installConfigCreds.Diagnostics == nil && vcenterCreds.Diagnostics != nil {
installConfigCreds.Diagnostics = &vsphere.AccountCredentials{
Username: vcenterCreds.Diagnostics.Username,
Password: vcenterCreds.Diagnostics.Password,
VCenter: vCenterFQDN,
}
}

return installConfigCreds
}
Loading