Skip to content
82 changes: 82 additions & 0 deletions cmd/openshift-install/migrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"testing"
)

// Story #9: Brownfield Migration Tooling
// Integration tests for migration orchestration and rollback logic

// TestMigration_HappyPath verifies the complete migration workflow
// from passthrough mode to per-component mode.
// AC: All components migrate successfully with proper secrets, CCO config, and operator restarts.
func TestMigration_HappyPath(t *testing.T) {
t.Skip("Story #9: Test stub - implement happy path migration")
// Given: Existing cluster in passthrough mode with single admin account
// And: Valid credentials file with all 4 component accounts
// When: Administrator runs openshift-install vsphere migrate-to-per-component
// Then: Migration validates all component credentials
// And: Creates backup of original vsphere-cloud-credentials secret
// And: Creates 4 component-specific secrets
// And: Updates CCO configuration to per-component mode
// And: Restarts operators (Machine API, CSI, CCM, Diagnostics)
// And: All components reconnect successfully
// And: Logs "Migration completed successfully"
}

// TestMigration_MachineAPICredentialInvalid_Rollback verifies that
// migration rolls back when Machine API credentials are invalid.
// AC: Failed reconnection triggers rollback to original passthrough state.
func TestMigration_MachineAPICredentialInvalid_Rollback(t *testing.T) {
t.Skip("Story #9: Test stub - implement Machine API rollback")
// Given: Credentials file with invalid Machine API credentials
// When: Migration runs and Machine API operator fails to reconnect
// Then: Migration detects failure and restores original secret
// And: Reverts CCO configuration to passthrough mode
// And: Logs "Migration failed: machine-api reconnection failed. Rolled back."
// And: Cluster returns to original working state
}

// TestMigration_CSICredentialInvalid_Rollback verifies that
// migration rolls back when CSI Driver credentials are invalid.
// AC: Failed CSI reconnection triggers full rollback per acceptance criteria.
func TestMigration_CSICredentialInvalid_Rollback(t *testing.T) {
t.Skip("Story #9: Test stub - implement CSI rollback")
// Given: Credentials file with invalid CSI Driver credentials
// When: Migration runs and CSI Driver fails to reconnect
// Then: Migration rolls back to original passthrough-mode secret
// And: Logs "Migration failed: csi-driver reconnection failed. Rolled back."
}

// TestMigration_CCMCredentialInvalid_Rollback verifies that
// migration rolls back when Cloud Controller Manager credentials are invalid.
func TestMigration_CCMCredentialInvalid_Rollback(t *testing.T) {
t.Skip("Story #9: Test stub - implement CCM rollback")
// Given: Credentials file with invalid CCM credentials
// When: Migration runs and CCM operator fails to reconnect
// Then: Migration rolls back to original state
// And: Logs "Migration failed: cloud-controller reconnection failed. Rolled back."
}

// TestMigration_DiagnosticsCredentialInvalid_Rollback verifies that
// migration rolls back when Diagnostics credentials are invalid.
func TestMigration_DiagnosticsCredentialInvalid_Rollback(t *testing.T) {
t.Skip("Story #9: Test stub - implement Diagnostics rollback")
// Given: Credentials file with invalid Diagnostics credentials
// When: Migration runs and Diagnostics component fails to reconnect
// Then: Migration rolls back to original state
// And: Logs "Migration failed: diagnostics reconnection failed. Rolled back."
}

// TestMigration_OperatorRestartVerification verifies that all
// component operators restart successfully and reach Ready state.
// AC: Operators must restart within 5 minutes and reconnect with new credentials.
func TestMigration_OperatorRestartVerification(t *testing.T) {
t.Skip("Story #9: Test stub - implement operator restart verification")
// Given: Successful migration completes
// When: Verifying operator restarts
// Then: Machine API operator deployment rollout completes
// And: CSI Driver daemonset pods restart
// And: CCM deployment rollout completes
// And: All operators reach Ready state within 5 minutes
}
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