Classes/VMWare.VCenterManager.psm1

using module .\VMWare.AuthType.psm1
using module .\VMWare.VCenter.psm1
using module .\VMWare.Settings.psm1

class VCenterManager {
    # Singleton Storage
    static [VCenterManager] $Instance

    # Saved vCenter profiles and the currently active one
    [System.Collections.Generic.List[VMWareVCenter]]$Profiles
    [String]$ActiveVSphere

    VCenterManager() {
        $this.Profiles = [System.Collections.Generic.List[VMWareVCenter]]::new()
    }

    static [Void] ClearInstance() {
        [VCenterManager]::Instance = $null
    }

    # Get the in-memory instance, loading from disk or creating an empty one. Does not connect.
    static [VCenterManager] GetOrCreate() {
        if ($null -eq [VCenterManager]::Instance) {
            if ($null -eq [VCenterManager]::Load()) {
                [VCenterManager]::Instance = [VCenterManager]::new()
            }
        }
        return [VCenterManager]::Instance
    }

    # Get the instance and ensure the active vCenter is connected. Throws if nothing is saved.
    static [VCenterManager] GetInstance() {
        $auth = [VCenterManager]::GetOrCreate()
        if ($auth.Profiles.Count -eq 0) {
            throw 'You are not logged in. Please login with Login-VSphere.'
        }
        $auth.ConnectActive()
        return $auth
    }

    [VMWareVCenter] GetActiveProfile() {
        $active = $this.ActiveVSphere
        return ($this.Profiles | Where-Object { $_.VSphere -eq $active } | Select-Object -First 1)
    }

    [VMWareVCenter] GetProfile([String]$VSphere) {
        return ($this.Profiles | Where-Object { $_.VSphere -eq $VSphere } | Select-Object -First 1)
    }

    # Add a new profile or replace an existing one with the same FQDN.
    [Void] AddOrUpdate([VMWareVCenter]$VCenter) {
        $existing = $this.GetProfile($VCenter.VSphere)
        if ($null -ne $existing) {
            [void]$this.Profiles.Remove($existing)
        }
        $this.Profiles.Add($VCenter)
    }

    [Void] Remove([String]$VSphere) {
        $existing = $this.GetProfile($VSphere)
        if ($null -eq $existing) {
            throw "No saved vCenter found for '$VSphere'."
        }
        [void]$this.Profiles.Remove($existing)
        if ($this.ActiveVSphere -eq $VSphere) {
            $this.ActiveVSphere = ''
        }
    }

    [Bool] IsConnected([String]$VSphere) {
        $servers = Get-Variable -Name 'DefaultVIServers' -Scope 'Global' -ValueOnly -ErrorAction SilentlyContinue
        $connection = $servers | Where-Object { $_.Name -eq $VSphere -and $_.IsConnected }
        return ($null -ne $connection)
    }

    [Void] Disconnect([String]$VSphere) {
        $servers = Get-Variable -Name 'DefaultVIServers' -Scope 'Global' -ValueOnly -ErrorAction SilentlyContinue
        $connections = $servers | Where-Object { $_.Name -eq $VSphere }
        foreach ($server in $connections) {
            Disconnect-VIServer -Server $server -Force -Confirm:$false -ErrorAction SilentlyContinue
        }
    }

    [Void] DisconnectAllExcept([String]$VSphere) {
        $servers = Get-Variable -Name 'DefaultVIServers' -Scope 'Global' -ValueOnly -ErrorAction SilentlyContinue
        $connections = $servers | Where-Object { $_.Name -ne $VSphere }
        foreach ($server in $connections) {
            Disconnect-VIServer -Server $server -Force -Confirm:$false -ErrorAction SilentlyContinue
        }
    }

    # Connect the active vCenter if it is not already connected.
    [Void] ConnectActive() {
        $active = $this.GetActiveProfile()
        if ($null -eq $active) {
            throw 'No active vCenter is selected. Use Switch-VSphereLogin to select one.'
        }
        if (-not $this.IsConnected($active.VSphere)) {
            $active.Connect()
        }
    }

    # Make a saved vCenter the active one: disconnect any others and connect the target.
    [Void] SetActive([String]$VSphere) {
        $target = $this.GetProfile($VSphere)
        if ($null -eq $target) {
            throw "No saved vCenter found for '$VSphere'."
        }
        $this.DisconnectAllExcept($VSphere)
        $target.Connect()
        $this.ActiveVSphere = $VSphere
    }

    # Persist the settings to the local system.
    [Void] Save() {
        if (!(Test-Path([VMWareSettings]::SAVE_DIR))) {
            New-Item -Type Directory -Path ([VMWareSettings]::SAVE_DIR) | Out-Null
        }

        $savedProfiles = @()
        foreach ($vcenter in $this.Profiles) {
            $savedProfiles += [PSCustomObject]@{
                VSphere                  = $vcenter.VSphere
                AuthType                 = $vcenter.AuthType.ToString()
                Credential               = $(if ($vcenter.AuthType -eq [VMWareAuthType]::Credential) { $vcenter.Credential } else { $null })
                ClientId                 = $vcenter.ClientId
                ClientSecret             = $(if ($vcenter.AuthType -eq [VMWareAuthType]::SSO) { $vcenter.ClientSecret } else { $null })
                AuthorizationEndpointUrl = $(if ($vcenter.AuthorizationEndpointUrl) { $vcenter.AuthorizationEndpointUrl.ToString() } else { $null })
                TokenEndpointUrl         = $(if ($vcenter.TokenEndpointUrl) { $vcenter.TokenEndpointUrl.ToString() } else { $null })
                RedirectUrl              = $(if ($vcenter.RedirectUrl) { $vcenter.RedirectUrl.ToString() } else { $null })
                Scope                    = $vcenter.Scope
                IgnoreCertificateErrors  = $vcenter.IgnoreCertificateErrors
            }
        }

        $save = [PSCustomObject]@{
            Version       = [VMWareSettings]::SCHEMA_VERSION
            ActiveVSphere = $this.ActiveVSphere
            Profiles      = @($savedProfiles)
        }

        $save | Export-Clixml -Path ([VMWareSettings]::SAVE_PATH) -Encoding 'utf8' -Force
    }

    # Persist only if a save file already exists (respects the opt-in save model).
    [Void] SaveIfPersisted() {
        if (Test-Path([VMWareSettings]::SAVE_PATH)) {
            $this.Save()
        }
    }

    # Remove the saved settings file from the local system.
    static [Void] Delete() {
        if (Test-Path([VMWareSettings]::SAVE_PATH)) {
            $file = Get-Item -Path ([VMWareSettings]::SAVE_PATH)
            if ($file.Name -eq [VMWareSettings]::SAVE_FILE) {
                Remove-Item -Path ([VMWareSettings]::SAVE_PATH) -Force
            }
        }
    }

    # Load saved settings, migrating older schema versions as needed.
    hidden static [VCenterManager] Load() {
        if (-not (Test-Path([VMWareSettings]::SAVE_PATH))) {
            return $null
        }

        try {
            $import = Import-Clixml -Path ([VMWareSettings]::SAVE_PATH)
        }
        catch {
            Write-Warning 'Failed to load settings'
            return $null
        }

        # Version 1 files predate the Version property.
        $version = if ($null -ne $import.Version) { [Int]$import.Version } else { 1 }

        if ($version -gt [VMWareSettings]::SCHEMA_VERSION) {
            throw "The saved vCenter profile file was written by a newer version of this module (schema version $version). This module supports up to schema version $([VMWareSettings]::SCHEMA_VERSION). To use this older module version you must delete the profile file at: $([VMWareSettings]::SAVE_PATH)"
        }

        $auth = [VCenterManager]::new()

        if ($version -eq 1) {
            $auth.ImportV1($import)
        }
        else {
            $auth.ImportCurrent($import)
        }

        [VCenterManager]::Instance = $auth

        # Rewrite older files in the current schema.
        if ($version -lt [VMWareSettings]::SCHEMA_VERSION) {
            $auth.Save()
        }

        return $auth
    }

    # Migrate a version 1 (single-profile) file into the current format.
    hidden [Void] ImportV1([Object]$import) {
        $vcenter = [VMWareVCenter]::new()
        $vcenter.VSphere = $import.VSphere
        if ($null -ne $import.Credential) {
            $vcenter.Credential = $import.Credential
            $vcenter.AuthType = [VMWareAuthType]::Credential
        }
        else {
            $vcenter.AuthType = [VMWareAuthType]::Integrated
        }
        $this.Profiles.Add($vcenter)
        $this.ActiveVSphere = $import.VSphere
    }

    hidden [Void] ImportCurrent([Object]$import) {
        foreach ($saved in $import.Profiles) {
            $vcenter = [VMWareVCenter]::new()
            $vcenter.VSphere = $saved.VSphere
            $vcenter.AuthType = [VMWareAuthType]$saved.AuthType
            $vcenter.Credential = $saved.Credential
            $vcenter.ClientId = $saved.ClientId
            $vcenter.ClientSecret = $saved.ClientSecret
            if ($saved.AuthorizationEndpointUrl) { $vcenter.AuthorizationEndpointUrl = [Uri]$saved.AuthorizationEndpointUrl }
            if ($saved.TokenEndpointUrl) { $vcenter.TokenEndpointUrl = [Uri]$saved.TokenEndpointUrl }
            if ($saved.RedirectUrl) { $vcenter.RedirectUrl = [Uri]$saved.RedirectUrl }
            $vcenter.Scope = $saved.Scope
            $vcenter.IgnoreCertificateErrors = [Bool]$saved.IgnoreCertificateErrors
            $this.Profiles.Add($vcenter)
        }
        $this.ActiveVSphere = $import.ActiveVSphere
    }
}