Public/Move-DockerStorage.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<#
    .SYNOPSIS
        This function moves Docker-For-Windows (DockerCE) Windows and Linux Container storage
        to the specified location(s).
 
    .DESCRIPTION
        See .SYNOPSIS
 
    .PARAMETER NewDockerDrive
        This parameter is OPTIONAL.
 
        This parameter takes a letter (A-Z) that represents the drive that you would like to move
        Windows and Linux Container storage to.
 
        If you use this parameter, do not use the -CustomWindowsImageStoragePath or -CustomLinuxImageStoragePath
        parameters.
 
    .PARAMETER CustomWindowsImageStoragePath
        This parameter is OPTIONAL.
 
        This parameter takes a string that represents a full path to a directory where you would like
        Windows Container storage moved.
 
        Do not use this parameter if you are using the -NewDockerDrive parameter.
 
    .PARAMETER CustomLinuxImageStoragePath
        This parameter is OPTIONAL.
 
        This parameter takes a string that represents a full path to a directory where you would like
        Linux Container storage moved.
 
        Do not use this parameter if you are using the -NewDockerDrive parameter.
 
    .PARAMETER MoveWindowsImagesOnly
        This parameter is OPTIONAL.
 
        This parameter is a switch. If used, only Windows Container storage will be moved.
 
    .PARAMETER MoveLinuxImagesOnly
        This parameter is OPTIONAL.
 
        This parameter is a switch. If used, only Linux Container storage will be moved.
 
    .PARAMETER Force
        This parameter is OPTIONAL.
 
        This parameter is a switch.
 
        Moving Windows Docker Storage to a new location causes existing Windows docker images and containers to be removed.
        Default behavior for this function is prompt the user to confirm this action before proceeding. Use the -Force
        switch to skip this prompt.
 
    .EXAMPLE
        # Open an elevated PowerShell Session, import the module, and -
 
        PS C:\Users\zeroadmin> Move-DockerStorage -NewDockerDrive H -Force
         
#>

function Move-DockerStorage {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [ValidatePattern("[a-zA-Z]")]
        [string]$NewDockerDrive,

        [Parameter(Mandatory=$False)]
        [string]$CustomWindowsImageStoragePath,

        [Parameter(Mandatory=$False)]
        [string]$CustomLinuxImageStoragePath,

        [Parameter(Mandatory=$False)]
        [switch]$MoveWindowsImagesOnly,

        [Parameter(Mandatory=$False)]
        [switch]$MoveLinuxImagesOnly,

        [Parameter(Mandatory=$False)]
        [switch]$Force
    )

    # Make sure one of the parameters is used
    if (!$NewDockerDrive -and !$CustomWindowsImageStoragePath -and !$CustomLinuxImageStoragePath) {
        Write-Error "The $($MyInvocation.MyCommand.Name) function requires either the -NewDockerDrive parameter or the -CustomDockerStoragePath parameter! Halting!"
        $global:FunctionResult = "1"
        return
    }
    if ($CustomWindowsImageStoragePath -and $MoveLinuxImagesOnly) {
        Write-Error "The switch -MoveLinuxImagesOnly was used in conjunction with the -CustomWindowsImageStoragePath parameter! Halting!"
        $global:FunctionResult = "1"
        return
    }
    if ($CustomLinuxImageStoragePath -and $MoveWindowsImagesOnly) {
        Write-Error "The switch -MoveWindowsImagesOnly was used in conjunction with the -CustomLinuxImageStoragePath parameter! Halting!"
        $global:FunctionResult = "1"
        return
    }

    # Make sure docker is installed
    if (![bool]$(Get-Command docker -ErrorAction SilentlyContinue)) {
        Write-Error "The 'docker' command is not available! Is docker installed? Halting!"
        $global:FunctionResult = "1"
        return
    }

    $DockerInfo = Get-DockerInfo
    $LocalDrives = Get-CimInstance Win32_LogicalDisk | Where-Object {$_.Drivetype -eq 3} | foreach {Get-PSDrive $_.DeviceId[0] -ErrorAction SilentlyContinue}

    if ($NewDockerDrive) {
        while ($LocalDrives.Name -notcontains $NewDockerDrive) {
            Write-Warning "$NewDockerDrive is not a valid Local Drive!"
            $NewDockerDrive = Read-Host -Prompt "Please enter the drive letter (LETTER ONLY) that you would like to move Docker Storage to [$($LocalDrives.Name -join '|')]"
        }
    }

    if ($CustomLinuxImageStoragePath) {
        if ($NewDockerDrive) {
            if ($CustomLinuxImageStoragePath[0] -ne $NewDockerDrive) {
                Write-Error "The drive indicated by -CustomLinuxImageStoragePath (i.e. $($CustomLinuxImageStoragePath[0])) is not the same as the drive indicated by -NewDockerDrive (i.e. $NewDockerDrive)! Halting!"
                $global:FunctionResult = "1"
                return
            }
        }

        $FinalLinuxImageStoragePath = $CustomLinuxImageStoragePath
    }
    else {
        $FinalLinuxImageStoragePath = "$NewDockerDrive`:\DockerStorage\LinuxContainers"
    }
    if (!$(Test-Path $FinalLinuxImageStoragePath)) {
        $null = New-Item -ItemType Directory -Path $FinalLinuxImageStoragePath -Force
    }

    if ($CustomWindowsImageStoragePath) {
        if ($NewDockerDrive) {
            if ($CustomWindowsImageStoragePath[0] -ne $NewDockerDrive) {
                Write-Error "The drive indicated by -CustomWindowsImageStoragePath (i.e. $($CustomWindowsImageStoragePath[0])) is not the same as the drive indicated by -NewDockerDrive (i.e. $NewDockerDrive)! Halting!"
                $global:FunctionResult = "1"
                return
            }
        }

        $FinalWindowsImageStoragePath = $CustomWindowsImageStoragePath
    }
    else {
        $FinalWindowsImageStoragePath = "$NewDockerDrive`:\DockerStorage\WindowsContainers"
    }
    if (!$(Test-Path $FinalWindowsImageStoragePath)) {
        $null = New-Item -ItemType Directory -Path $FinalWindowsImageStoragePath -Force
    }

    if (!$MoveLinuxImagesOnly) {
        # Unfortunately, it does not seem possible to move Docker Storage along with existing Windows docker images and containers.
        # So, the docker images and containers will need to be recreated after "C:\ProgramData\Docker\config\daemon.json"
        # has been updated with the new storage location
        if (!$Force) {
            Write-Warning "Moving Windows Docker Storage to a new location will remove all existing Windows docker images and containers!"
            $MoveWinDockerStorageChoice = Read-Host -Prompt "Are you sure you want to move Windows Docker Storage? [Yes\No]"
            while ($MoveWinDockerStorageChoice -match "Yes|yes|Y|y|No|no|N|n") {
                Write-Host "'$MoveWinDockerStorageChoice' is *not* a valid choice. Please enter either 'Yes' or 'No'."
                $MoveWinDockerStorageChoice = Read-Host -Prompt "Are you sure you want to move Windows Docker Storage? [Yes\No]"
            }

            if ($MoveWinDockerStorageChoice -notmatch "Yes|yes|Y|y") {
                Write-Warning "Windows Docker Storage will NOT be moved."
            }
        }

        if ($Force -or $MoveWinDockerStorageChoice -match "Yes|yes|Y|y") {
            # If "C:\ProgramData\Docker\config" doesn't exist, that means that Docker has NEVER been switched to Windows Containers
            # on this machine, so we need to do so.
            if (!$(Test-Path "C:\ProgramData\Docker\config")) {
                $null = Switch-DockerContainerType -ContainerType Windows
                #Write-Host "Sleeping for 30 seconds to give docker time to setup for Windows Containers..."
                #Start-Sleep -Seconds 30
                
                $UpdatedDockerInfo = Get-DockerInfo
                if ($UpdatedDockerInfo.DockerServerInfo.'OS/Arch' -notmatch "windows") {
                    Write-Error "Docker did NOT successfully switch to Windows Containers within the alotted time! Halting!"
                    $global:FunctionResult = "1"
                    return
                }
            }

            # Update the Docker Config File with the New Storage Location
            # Solution from:
            # https://social.technet.microsoft.com/Forums/Lync/en-US/4ac564e2-ad6d-4d32-8cb4-7fea481738a4/how-to-change-docker-images-and-containers-location-with-windows-containers?forum=ws2016
            
            $DockerConfigJsonAsPSObject = Get-Content "C:\ProgramData\Docker\config\daemon.json" | ConvertFrom-Json
            $DockerConfigJsonAsPSObject | Add-Member -Type NoteProperty -Name data-root -Value $FinalWindowsImageStoragePath -Force
            $DockerConfigJsonAsPSObject | ConvertTo-Json -Compress | Out-File "C:\ProgramData\Docker\config\daemon.json"
        }
    }

    if (!$MoveWindowsImagesOnly) {
        # We need to move the MobyLinuxVM #

        # Make sure that com.docker.service is Stopped and 'Docker For Windows.exe' and 'dockerd.exe' are not running
        try {
            $DockerService = Get-Service com.docker.service -ErrorAction Stop

            if ($DockerService.Status -ne "Stopped") {
                $DockerService | Stop-Service -Force
            }
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }

        try {
            $DockerForWindowsProcess = Get-Process "Docker For Windows" -ErrorAction SilentlyContinue
            if ($DockerForWindowsProcess) {
                $DockerForWindowsProcess | Stop-Process -Force
            }

            $DockerDProcess = Get-Process "dockerd" -ErrorAction SilentlyContinue
            if ($DockerDProcess) {
                $DockerDProcess | Stop-Process -Force
            }
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }

        # Make sure the MobyLinuxVM is Off
        $MobyLinuxVMInfo = Get-VM -Name MobyLinuxVM

        if ($MobyLinuxVMInfo.State -ne "Off") {
            try {
                Stop-VM -VMName MobyLinuxVM -TurnOff -Confirm:$False -Force -ErrorAction Stop
            }
            catch {
                Write-Error $_
                $global:FunctionResult = "1"
                return
            }
        }

        $DockerSettings = Get-Content "$env:APPDATA\Docker\settings.json" | ConvertFrom-Json
        $DockerSettings.MobyVhdPathOverride = "$CustomLinuxImageStoragePath\MobyLinuxVM.vhdx"
        $DockerSettings | ConvertTo-Json | Out-File "$env:APPDATA\Docker\settings.json"

        # Alternate method using symlink/junction
        <#
        try {
            # Get the current location of the VHD - It's almost definitely under
            # C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks\MobyLinuxVM.vhdx, but check to make sure
            $MLVhdLocation = $(Get-VMHardDiskDrive -VMName MobyLinuxVM).Path
            $MLVhdLocationParentDir = $MLVhdLocation | Split-Path -Parent
 
            # Determine if there are other files in $MLVhdLocationParentDir. If there are, halt because
            # we can't safely create the symlink, so just leave everything where it is...
            $MLVhdLocationContentCheck = Get-ChildItem -Path $MLVhdLocationParentDir -File -Recurse
            if ($MLVhdLocationContentCheck.Count -gt 1 -and
            [bool]$($MLVhdLocationContentCheck.FullName -notmatch "MobyLinuxVM.*?vhdx$")
            ) {
                Write-Warning "There are files under '$MLVhdLocationParentDir' besides VHDs related to MobyLinuxVM! MobyLinuxVM storage will NOT be moved!"
            }
            else {
                Move-VMStorage -VMName MobyLinuxVM -DestinationStoragePath $FinalLinuxImageStoragePath
 
                Write-Host "Removing empty directory '$MLVhdLocationParentDir' in preparation for symlink ..."
                Remove-Item $MLVhdLocationParentDir -ErrorAction Stop
                 
                Write-Host "Creating junction for MobyLinuxVM storage directory from original location '$MLVhdLocationParentDir' to new location '$FinalLinuxImageStoragePath\Virtual Hard Disks' ..."
                $null = cmd /c mklink /j $MLVhdLocationParentDir "$FinalLinuxImageStoragePath\Virtual Hard Disks"
            }
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }
        #>


        # Turn On Docker Again
        try {
            $DockerService = Get-Service com.docker.service -ErrorAction Stop

            if ($DockerService.Status -eq "Stopped") {
                $DockerService | Start-Service
                Write-Host "Sleeping for 30 seconds to give the com.docker.service service time to become ready..."
                Start-Sleep -Seconds 30
            }

            $MobyLinuxVMInfo = Get-VM -Name MobyLinuxVM
            if ($MobyLinuxVMInfo.State -ne "Running") {
                Write-Host "Manually starting MobyLinuxVM..."
                Start-VM -Name MobyLinuxVM
            }

            & "C:\Program Files\Docker\Docker\Docker For Windows.exe"
        }
        catch {
            Write-Error $_
            $global:FunctionResult = "1"
            return
        }

        # Make sure we switch to Linuc Container Mode to ensure MobyLinuxVM is recreated in he new Storage Location
        $null = Switch-DockerContainerType -ContainerType Linux
    }

    
    # Create Output
    if ($FinalLinuxImageStoragePath) {
        $LinuxStorage = $FinalLinuxImageStoragePath
    }
    if ($FinalWindowsImageStoragePath) {
        $WindowsStorage = $FinalWindowsImageStoragePath
    }
    if ($CustomLinuxImageStoragePath) {
        $LinuxStorage = $CustomLinuxImageStoragePath
    }
    if ($CustomWindowsImageStoragePath) {
        $WindowsStorage = $CustomWindowsImageStoragePath
    }

    $Output = @{}

    if ($LinuxStorage) {
        $Output.Add("LinuxStorage",$LinuxStorage)
    }
    if ($WindowsStorage) {
        $Output.Add("WindowsStorage",$WindowsStorage)
    }

    [pscustomobject]$Output
}