DockerMachine.psm1

<#
 .Synopsis
  Create new, list or load existing Docker Machine environments.
#>



#region Init Module

$ErrorActionPreference = "Stop";
$DEFAULT_PORT = '443';

Try { & docker-machine | Out-Null } Catch { throw "docker-machine is not installed" }

#endregion
#region Private Functions

function Get-ConfigTemplate ($targetHost, $envName, $envDir) {
    # Generate target address
    $targetHost = $targetHost -replace 'tcp://','';
    if ($targetHost.Contains(':')) {
        $targetAddress = $targetHost;
        $targetHost = $targetHost.Split(':')[0];
    } else {
        $targetAddress = "${targetHost}:${DEFAULT_PORT}";
    }

    # Return config
    return @"
    {
        "ConfigVersion": 3,
        "Driver": {
            "IPAddress": "$targetHost",
            "MachineName": "$envName",
            "SSHUser": "",
            "SSHPort": 0,
            "SSHKeyPath": "",
            "SwarmMaster": false,
            "SwarmHost": "",
            "SwarmDiscovery": "",
            "URL": "tcp://${targetAddress}"
        },
        "DriverName": "none",
        "HostOptions": {
            "Driver": "",
            "Memory": 0,
            "Disk": 0,
            "EngineOptions": {
                "ArbitraryFlags": [],
                "Dns": null,
                "GraphDir": "",
                "Env": [],
                "Ipv6": false,
                "InsecureRegistry": [],
                "Labels": [],
                "LogLevel": "",
                "StorageDriver": "",
                "SelinuxEnabled": false,
                "TlsVerify": true,
                "RegistryMirror": [],
                "InstallURL": "https://get.docker.com"
            },
            "SwarmOptions": {
                "IsSwarm": false,
                "Address": "",
                "Discovery": "",
                "Agent": false,
                "Master": false,
                "Host": "tcp://0.0.0.0:3376",
                "Image": "swarm:latest",
                "Strategy": "spread",
                "Heartbeat": 0,
                "Overcommit": 0,
                "ArbitraryFlags": [],
                "ArbitraryJoinFlags": [],
                "Env": null,
                "IsExperimental": false
            },
            "AuthOptions": {
                "CertDir": "$($envDir -replace '\\', '\\')",
                "CaCertPath": "$($envDir -replace '\\', '\\')\\ca.pem",
                "CaCertRemotePath": "",
                "ClientKeyPath": "$($envDir -replace '\\', '\\')\\key.pem",
                "ServerCertRemotePath": "",
                "ServerKeyRemotePath": "",
                "ClientCertPath": "$($envDir -replace '\\', '\\')\\cert.pem",
                "ServerCertSANs": [],
                "StorePath": "$($envDir -replace '\\', '\\')"
            }
        },
        "Name": "$envName"
    }
"@

}

#endregion
#region Public Functions

function New-DockerMachine {

    <#
    .Synopsis
     Create new Docker Machine environment.
 
    .Description
     Creates new Docker Machine environment.
 
    .Parameter Name
     Name of the environment.
 
    .Parameter TargetHost
     Hostname of a Docker host. If no port is specified, default port 443 is used.
     Specify a custom port like this: "host.example.com:2376"
 
    .Parameter ClientBundleFile
     Client bundle file you downloaded from UCP. Not required if parameters -RootCaPemFile, -KeyPemFile, and -CertPemFile are used.
 
    .Parameter RootCaPemFile
     Root CA file in PEM format. Not required if parameter -ClientBundleFile is used.
 
    .Parameter KeyPemFile
     Private key file in PEM format. Not required if parameter -ClientBundleFile is used.
 
    .Parameter CertPemFile
     Certificate file in PEM format. Not required if parameter -ClientBundleFile is used.
 
    .Example
     # Create new environment 'example'. Access Docker API with TLS on host.example.com:443. Certificates from given client bundle will be used.
     New-DockerMachine -name example -TargetHost host.example.com -ClientBundleFile .\ucp-bundle-user.zip
 
    .Example
     # Create new environment 'example'. Access Docker API with TLS on host.example.com:2376, not to default port 443. Certificates from given client bundle will be used.
     New-DockerMachine -name example -TargetHost "host.example.com:2376" -ClientBundleFile .\ucp-bundle-user.zip
 
    .Example
     # Create new environment 'example'. Access Docker API with TLS on host.example.com:443. Instead of a client bundle three separate certificate files in PEM format will be used.
     New-DockerMachine -Name example -TargetHost host.example.com -RootCaPemFile .\cafile.pem -KeyPemFile .\keyfile.pem -CertPemFile .\certfile.pem
 
    .Example
     # Another scenario is possible: your IT department handles the certificate management for your Docker hosts. You send them your CSR and receive a custom client bundle.
     # That client bundle contains the root CA and the public certificate files, but not the private key file you created together with your CSR.
     # For Docker Machine to work, you additionally have to specify the key file. Should the client bundle contain a key file after all, it will be overwritten.
     New-DockerMachine -name example -TargetHost host.example.com -ClientBundleFile .\ucp-bundle-user.zip -KeyPemFile .\keyfile.pem
    #>


    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]

    Param (
        [Parameter(Mandatory=$True)][string]$Name,
        [Parameter(Mandatory=$True)][string]$TargetHost,
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})][Parameter(Mandatory=$False)][string]$ClientBundleFile,
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})][Parameter(Mandatory=$False)][string]$RootCaPemFile,
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})][Parameter(Mandatory=$False)][string]$CertPemFile,
        [ValidateScript({Test-Path -Path $_ -PathType Leaf})][Parameter(Mandatory=$False)][string]$KeyPemFile
    )

    $dockerMachineDir = Join-Path -Path $env:USERPROFILE -ChildPath ".docker\machine"
    $dockerMachinesDir = Join-Path -Path $dockerMachineDir -ChildPath "machines"
    $dockerEnvDir = Join-Path -Path $dockerMachinesDir -ChildPath $Name
    $dockerEnvConfig = Join-Path -Path $dockerEnvDir -ChildPath "config.json";
    $dockerEnvRootCa = Join-Path -Path $dockerEnvDir -ChildPath "ca.pem";
    $dockerEnvKey = Join-Path -Path $dockerEnvDir -ChildPath "key.pem";
    $dockerEnvCert = Join-Path -Path $dockerEnvDir -ChildPath "cert.pem";

    if (Test-Path -Path $dockerEnvDir -PathType Container) {
        Write-Warning "The environment `"$Name`" already exists at `"$dockerMachinesDir`". It must be removed first.";
        Remove-Item -Path $dockerEnvDir -Confirm:$true -Recurse -Force;

        if (Test-Path -Path $dockerEnvDir -PathType Container) {
            Write-Warning "Old environment was not removed. Exiting..."; exit 1;
        }
    }

    Write-Output "Creating environment at `"$dockerEnvDir`""
    if ($PSCmdlet.ShouldProcess($dockerEnvDir, "Create directory")) {
        New-Item -ItemType Directory -Path $dockerEnvDir | Out-Null;
    }

    try {
        Write-Verbose "Extract/copy certificates";
        if ($PSCmdlet.ShouldProcess($dockerEnvDir, "Extract/copy certificates")) {
            if ($ClientBundleFile -ne "") { Expand-Archive $ClientBundleFile -DestinationPath $dockerEnvDir; } # BUG: Nicht mit relativem Pfad
            if ($RootCaPemFile -ne "") { Copy-Item -Path $RootCaPemFile -Destination $dockerEnvRootCa -Confirm:$false; }
            if ($KeyPemFile -ne "") { Copy-Item -Path $KeyPemFile -Destination $dockerEnvKey -Confirm:$false; }
            if ($CertPemFile -ne "") { Copy-Item -Path $CertPemFile -Destination $dockerEnvCert -Confirm:$false; }
            if ((Test-Path -Path $dockerEnvRootCa -PathType Leaf) -and (Test-Path -Path $dockerEnvKey -PathType Leaf) -and (Test-Path -Path $dockerEnvCert -PathType Leaf)) {
                Write-Verbose "Certificates extracted/copied successfully";
            } else {
                throw "Certificate(s) missing. Make sure to pass a valid client bundle file with parameter -ClientBundleFile or pass individual certificate files in PEM format with parameters -RootCaPemFile, -KeyPemFile, and -CertPemFile.";
            }
        }

        Write-Verbose "Generating docker config.json"
        $configTemplate = Get-ConfigTemplate -targetHost $TargetHost -envName $Name -envDir $dockerEnvDir;
        if ($PSCmdlet.ShouldProcess($dockerEnvDir, "Write config file")) {
            $configTemplate | Set-Content $dockerEnvConfig;
        }
        
        Write-Output "Environment `"$Name`" is now ready."
        Write-Output "Type `"Use-DockerMachine $Name`" to switch to that environment."
        Write-Warning "Be careful when interacting with a remote environment."
    }
    catch {
        Write-Verbose "Cleanup ${$dockerEnvDir}";
        Remove-Item -Path $dockerEnvDir -Recurse -Force
        throw $_;
    }

}

function Use-DockerMachine {

    <#
    .Synopsis
     Use Docker Machine environment.
 
    .Description
     Use Docker Machine environment. Afterwards you can type Docker commands for this host/swarm.
 
    .Parameter Name
     Name of the environment.
 
    .Example
     # Get a list of existing environments to choose from
     Use-DockerMachine
      
     # Use environment "test":
     Use-DockerMachine test
 
    .Example
     # Use local Docker host:
     Use-DockerMachine local
    #>


    Param(
        [Parameter(Mandatory=$False)][string]$Name
    )

    if (!$Name) {
        $Name = Select-DockerMachine
    }

    if ($Name -eq "local" -or $Name -eq "default") {
        if (Test-Path Env:\\DOCKER_HOST) {
            Remove-Item Env:\\DOCKER_TLS_VERIFY;
            Remove-Item Env:\\DOCKER_HOST;
            Remove-Item Env:\\DOCKER_CERT_PATH;
            Remove-Item Env:\\DOCKER_MACHINE_NAME;
        }
    } else {
        Try {
            $dmDockerHost = docker-machine url $Name 2>&1;
        } Catch {
            throw "Docker Machine environment '${Name}' does not exist!"; exit 1;
        }
        [System.Environment]::SetEnvironmentVariable("DOCKER_TLS_VERIFY", "1", "Process");
        [System.Environment]::SetEnvironmentVariable("DOCKER_HOST", $dmDockerHost, "Process");
        [System.Environment]::SetEnvironmentVariable("DOCKER_CERT_PATH", "$env:userprofile\.docker\machine\machines\$Name", "Process");
        [System.Environment]::SetEnvironmentVariable("DOCKER_MACHINE_NAME", $Name, "Process");
        [System.Environment]::SetEnvironmentVariable("COMPOSE_CONVERT_WINDOWS_PATHS", "true", "Process");
    }

}

function Select-DockerMachine {
    $dockerMachinesDir = Join-Path -Path $env:USERPROFILE -ChildPath ".docker\machine\machines"
    $dockerEnvs = Get-ChildItem $dockerMachinesDir â€“recurse -directory | Sort-Object
    
    Write-Host "Please choose a docker environment:"

    $table = @()
    $table += New-EnvObj -Index 1 -Name local -Target default

    foreach($dockerEnv in $dockerEnvs) {
        $index = $dockerEnvs.IndexOf($dockerEnv)
        $target = Get-Content (Join-Path -Path $dockerMachinesDir -ChildPath $dockerEnv | Join-Path -ChildPath "config.json") `
                  | Out-String | ConvertFrom-Json | Select-Object -Expand Driver | Select-Object -Expand URL
        $table += New-EnvObj -Index $($index+2) -Name $dockerEnv -Target $target
    }
    
    $table | Format-Table -AutoSize @{n=' # ';e={$_.Index};align='center'},`
                          Name,`
                          Target | Out-Host

    $choice = Read-Host "Your choice (#)"
    if ($choice -notmatch '\d' -or $choice -lt 1) {
        Write-Error "Invalid option"
        return
    }

    return $table[[int]$choice-1].Name
}

function New-EnvObj {
    Param(
        [Parameter(Mandatory=$True)][string]$Index,
        [Parameter(Mandatory=$True)][string]$Name,
        [Parameter(Mandatory=$True)][string]$Target
    )

    $envObj = New-Object PSObject
    $envObj | Add-Member NoteProperty Index ($Index) -TypeName [int]
    $envObj | Add-Member NoteProperty Name ($Name)
    $envObj | Add-Member NoteProperty Target ($Target)

    return $envObj
}

function Get-DockerMachine {

    <#
    .Synopsis
     Get list of Docker Machine environments.
 
    .Description
     Get list of currently configured Docker Machine environments.
 
    .Example
     Get-DockerMachines
    #>


    & docker-machine ls -q

}

function Remove-DockerMachine {

    <#
    .Synopsis
     Remove Docker Machine environment.
 
    .Description
     Remove Docker Machine environment.
 
    .Parameter Name
     Name of the environment.
 
    .Example
     # Remove environment "test":
     Remove-DockerMachine test
    #>


    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")]

    Param(
        [Parameter(Mandatory=$True)][string]$Name
    )

    if ($PSCmdlet.ShouldProcess($Name, "docker-machine rm")) {            
        & docker-machine rm -y $Name;
    }

}

#endregion
#region Export Module Members

Export-ModuleMember -Function ("New-DockerMachine", "Use-DockerMachine", "Get-DockerMachine", "Remove-DockerMachine");

#endregion