diff --git a/README.md b/README.md index 9dc6a7d..ab96b37 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ You will need to append the IP address of the master node to the Hosts file on t > You can find the IP address of the master Linux node by running the following command on your machine: > >```powershell ->minikube node list +>minikube ip >``` Your hosts file should look similar to this: diff --git a/auto-install.iso b/auto-install.iso new file mode 100644 index 0000000..87e8b75 Binary files /dev/null and b/auto-install.iso differ diff --git a/automation/ContainerdTools.psm1 b/automation/ContainerdTools.psm1 new file mode 100644 index 0000000..d06493c --- /dev/null +++ b/automation/ContainerdTools.psm1 @@ -0,0 +1,257 @@ +Import-Module -Name "$PSScriptRoot\SetUpUtilities.psm1" -Force + +function Get-ContainerdLatestVersion { + $latestVersion = Get-LatestToolVersion -Repository "containerd/containerd" + return $latestVersion +} + +function Install-Containerd { + param( + [String] + [parameter(HelpMessage = "Path to install containerd. Defaults to ~\program files\containerd")] + $InstallPath = "$Env:ProgramFiles\containerd", + + [String] + [parameter(HelpMessage = "Path to download files. Defaults to user's Downloads folder")] + $DownloadPath = "$HOME\Downloads" + ) + + # Uninstall if tool exists at specified location. Requires user consent + Uninstall-ContainerTool -Tool "ContainerD" -Path $InstallPath + + $Version = Get-ContainerdLatestVersion + + $Version = $Version.TrimStart('v') + Write-Output "* Downloading and installing Containerd v$version at $InstallPath" + "Downloading and installing Containerd v$version at $InstallPath" >> logs + + + # Download file from repo + $containerdTarFile = "containerd-${version}-windows-amd64.tar.gz" + try { + $Uri = "https://github.com/containerd/containerd/releases/download/v$version/$($containerdTarFile)" + Invoke-WebRequest -Uri $Uri -OutFile $DownloadPath\$containerdTarFile | Out-Null + } + catch { + Throw "Containerd download failed. $_" + } + + + # Untar and install tool + $params = @{ + Feature = "containerd" + InstallPath = $InstallPath + DownloadPath = "$DownloadPath\$containerdTarFile" + EnvPath = "$InstallPath\bin" + cleanup = $true + } + + + Install-RequiredFeature @params | Out-Null + + Write-Output "* Containerd v$version successfully installed at $InstallPath" + "Containerd v$version successfully installed at $InstallPath" >> logs + containerd.exe -v >> logs + + "For containerd usage: run 'containerd -h'" >> logs +} + +function Start-ContainerdService { + Set-Service containerd -StartupType Automatic + try { + Start-Service containerd + + # Waiting for containerd to come to steady state + (Get-Service containerd -ErrorAction SilentlyContinue).WaitForStatus('Running', '00:00:30') + } + catch { + Throw "Couldn't start Containerd service. $_" + } + + Write-Output "* Containerd is installed and the service is started ..." + "Containerd is installed and the service is started" >> logs + +} + +function Stop-ContainerdService { + $containerdStatus = Get-Service containerd -ErrorAction SilentlyContinue + if (!$containerdStatus) { + Write-Warning "Containerd service does not exist as an installed service." + "Containerd service does not exist as an installed service." >> logs + return + } + + try { + Stop-Service containerd -NoWait + + # Waiting for containerd to come to steady state + (Get-Service containerd -ErrorAction SilentlyContinue).WaitForStatus('Stopped', '00:00:30') + } + catch { + Throw "Couldn't stop Containerd service. $_" + } +} + +function Initialize-ContainerdService { + param( + [string] + [parameter(HelpMessage = "Containerd path")] + $ContainerdPath = "$Env:ProgramFiles\containerd" + ) + + "Configuring the containerd service" >> logs + + #Configure containerd service + $containerdConfigFile = "$ContainerdPath\config.toml" + $containerdDefault = containerd.exe config default + $containerdDefault | Out-File $ContainerdPath\config.toml -Encoding ascii + Write-Information -InformationAction Continue -MessageData "* Review containerd configutations at $containerdConfigFile ..." + + Add-MpPreference -ExclusionProcess "$ContainerdPath\containerd.exe" + + # Review the configuration. Depending on setup you may want to adjust: + # - the sandbox_image (Kubernetes pause image) + # - cni bin_dir and conf_dir locations + + + # Setting Old value New Value + # bin_dir "C:\\Program Files\\containerd\\cni\\bin" "c:\\opt\\cni\\bin" + # conf_dir "C:\\Program Files\\containerd\\cni\\conf" "c:\\etc\\cni\\net.d\\" + + # Read the content of the config.toml file + $containerdConfigContent = Get-Content -Path $containerdConfigFile -Raw + + # Define the replacements + $replacements = @( + @{ + Find = 'bin_dir = "C:\\Program Files\\containerd\\cni\\bin"' + Replace = 'bin_dir = "c:\\opt\\cni\\bin"' + }, + @{ + Find = 'conf_dir = "C:\\Program Files\\containerd\\cni\\conf"' + Replace = 'conf_dir = "c:\\etc\\cni\\net.d\\"' + } + ) + + # Perform the check and replacement in one loop + $replacementsMade = $false + foreach($replacement in $replacements) { + if ($containerdConfigContent -match [regex]::Escape($replacement.Find)) { + $containerdConfigContent = $containerdConfigContent -replace [regex]::Escape($replacement.Find), $replacement.Replace + $replacementsMade = $true + } + } + + # Write the modified content back to the config.toml file if any replacements were made + if ($replacementsMade) { + $containerdConfigContent | Set-Content -Path $containerdConfigFile + # Output a message indicating the changes + # Write-Host "Changes applied to $containerdConfigFile" + } else { + # Write-Host "No changes needed in $containerdConfigFile" + } + + # Create the folders if they do not exist + $binDir = "c:\opt\cni\bin" + $confDir = "c:\etc\cni\net.d" + + if (!(Test-Path $binDir)) { + mkdir $binDir | Out-Null + # Write-Host "Created $binDir" + } + + if (!(Test-Path $confDir)) { + mkdir $confDir | Out-Null + # Write-Host "Created $confDir" + } + + + $pathExists = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Machine) -like "*$ContainerdPath\bin*" + if (-not $pathExists) { + # Register containerd service + Add-FeatureToPath -Feature "containerd" -Path "$ContainerdPath\bin" + } + + # Check if the containerd service is already registered + $containerdServiceExists = Get-Service -Name "containerd" -ErrorAction SilentlyContinue + if (-not $containerdServiceExists) { + containerd.exe --register-service --log-level debug --service-name containerd --log-file "$env:TEMP\containerd.log" + if ($LASTEXITCODE -gt 0) { + Throw "Failed to register containerd service. $_" + } + } else { + Write-Host "Containerd service is already registered." + } + + Get-Service *containerd* | Select-Object Name, DisplayName, ServiceName, ServiceType, StartupType, Status, RequiredServices, ServicesDependedOn | Out-Null +} + +function Uninstall-Containerd { + param( + [string] + [parameter(HelpMessage = "Containerd path")] + $Path + ) + Write-Output "Uninstalling containerd" + + if (!$Path) { + $Path = Get-DefaultInstallPath -Tool "containerd" + } + + $pathItems = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue + if (!$pathItems.Name.Length) { + Write-Warning "Containerd does not exist at $Path or the directory is empty" + return + } + + try { + Stop-ContainerdService + } + catch { + Write-Warning "$_" + } + + # Unregister containerd service + Unregister-Containerd + + # Delete the containerd key + $regkey = "HKLM:\SYSTEM\CurrentControlSet\Services\containerd" + Get-Item -path $regkey -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force + + # Remove the folder where containerd service was installed + Get-Item -Path $Path -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force + + # Remove from env path + Remove-FeatureFromPath -Feature "containerd" + + Write-Output "Successfully uninstalled Containerd." +} +function Unregister-Containerd { + $scQueryResult = (sc.exe query containerd) | Select-String -Pattern "SERVICE_NAME: containerd" + if (!$scQueryResult) { + Write-Warning "Containerd service does not exist as an installed service." + return + } + # Unregister containerd service + containerd.exe --unregister-service + if ($LASTEXITCODE -gt 0) { + Write-Warning "Could not unregister containerd service. $_" + } + else { + Start-Sleep -Seconds 15 + } + + # # Delete containerd service + # sc.exe delete containerd + # if ($LASTEXITCODE -gt 0) { + # Write-Warning "Could not delete containerd service. $_" + # } +} + + +Export-ModuleMember -Function Get-ContainerdLatestVersion +Export-ModuleMember -Function Install-Containerd +Export-ModuleMember -Function Start-ContainerdService +Export-ModuleMember -Function Stop-ContainerdService -Alias Stop-Containerd +Export-ModuleMember -Function Initialize-ContainerdService +Export-ModuleMember -Function Uninstall-Containerd \ No newline at end of file diff --git a/automation/MinikubeTools.psm1 b/automation/MinikubeTools.psm1 new file mode 100644 index 0000000..314b568 --- /dev/null +++ b/automation/MinikubeTools.psm1 @@ -0,0 +1,49 @@ +Import-Module -Name "$PSScriptRoot\k8Tools.psm1" -Force + +function Get-JoinCommand { + param ( + [string] + $KubernetesVersion + ) + $JoinCommand = (minikube ssh "cd /var/lib/minikube/binaries/v$KubernetesVersion/ && sudo ./kubeadm token create --print-join-command") + $outputString = $JoinCommand -replace 'kubeadm', '.\kubeadm.exe' + $outputString += ' --cri-socket "npipe:////./pipe/containerd-containerd"' + $outputString += ' --v=5' + return $outputString + +} + +function Set-MinikubeFolderError { + if (!(Test-Path -Path c:\var\lib\minikube\certs)) { + mkdir c:\var\lib\minikube\certs | Out-Null + } + + if (Test-Path -Path C:\etc\kubernetes\pki\ca.crt) { + Copy-Item C:\etc\kubernetes\pki\ca.crt -Destination C:\var\lib\Minikube\Certs | Out-Null + Remove-Item C:\etc\kubernetes\pki\ca.crt | Out-Null + } else { + Write-Output "File C:\etc\kubernetes\pki\ca.crt does not exist." + } +} + +function Add-Host { + param ( + [string] + [ValidateNotNullOrEmpty()] + $IP, + [string] + [ValidateNotNullOrEmpty()] + $Path = "C:\Windows\System32\drivers\etc\hosts" + ) + + $entry = "`t$IP`tcontrol-plane.minikube.internal" + + $hostsContent = Get-Content -Path $Path -Raw -ErrorAction SilentlyContinue + if ($hostsContent -notmatch [regex]::Escape($entry)) { + Add-Content -Path $Path -Value "$entry" -Force | Out-Null + } +} + +Export-ModuleMember -Function Get-JoinCommand +Export-ModuleMember -Function Set-MinikubeFolderError +Export-ModuleMember -Function Add-Host \ No newline at end of file diff --git a/automation/NSSMTools.psm1 b/automation/NSSMTools.psm1 new file mode 100644 index 0000000..b168f10 --- /dev/null +++ b/automation/NSSMTools.psm1 @@ -0,0 +1,25 @@ +function Install-NSSM { + $nssmService = Get-WmiObject win32_service | Where-Object {$_.PathName -like '*nssm*'} + if ($nssmService) { + Write-Output "NSSM is already installed." + return + } + + if (-not (Test-Path -Path "c:\k" -PathType Container)) { + mkdir "c:\k" | Out-Null + } + $arch = "win64" + $nssmZipFile = "nssm-2.24.zip" + $nssmUri = "https://k8stestinfrabinaries.blob.core.windows.net/nssm-mirror/$nssmZipFile" + try { + Invoke-WebRequest -Uri $nssmUri -OutFile "c:\k\$nssmZipFile" | Out-Null + } + catch { + Throw "NSSM download failed. $_" + } + tar.exe C c:\k\ -xf "c:\k\$nssmZipFile" --strip-components 2 */$arch/*.exe | Out-Null + + Write-Output "* NSSM is installed ..." +} + +Export-ModuleMember -Function Install-NSSM \ No newline at end of file diff --git a/automation/Run.ps1 b/automation/Run.ps1 new file mode 100644 index 0000000..abcb667 --- /dev/null +++ b/automation/Run.ps1 @@ -0,0 +1,174 @@ +Import-Module -Name "$PSScriptRoot\k8Tools.psm1" -Force +Import-Module -Name "$PSScriptRoot\ContainerdTools.psm1" -Force +Import-Module -Name "$PSScriptRoot\MinikubeTools.psm1" -Force +Import-Module -Name "$PSScriptRoot\NSSMTools.psm1" -Force + +function Run { + param ( + [string]$VMName, + [string]$UserName, + [string]$Pass, + [System.Management.Automation.PSCredential]$Credential, + [string]$KubernetesVersion + ) + + # create and configure a new minikube cluster + Write-Output "* Creating and configuring a new minikube cluster ..." + & minikube start --driver=hyperv --hyperv-virtual-switch=$SwitchName --nodes=2 --cni=flannel --container-runtime=containerd --kubernetes-version=$KubernetesVersion + # & minikube start --driver=hyperv --hyperv-virtual-switch=$SwitchName --memory=4096 --cpus=2 --kubernetes-version=v1.20.2 --network-plugin=cni --cni=flannel --container-runtime=containerd --disk-size=15GB --wait=false >> logs + Write-Output "* Minikube cluster is created and configured ..." + # Prepare the Linux nodes for Windows-specific Flannel CNI configuration + # at the moment we are assuming that you only have two linux nodes named minikube and minikube-m02 + & minikube ssh "sudo sysctl net.bridge.bridge-nf-call-iptables=1 && exit" >> logs + & minikube ssh -n minikube-m02 "sudo sysctl net.bridge.bridge-nf-call-iptables=1 && exit" >> logs + Write-Output "* Linux nodes are ready for Windows-specific Flannel CNI configuration ..." + + + # configure Flannel CNI for Windows + # make sure the flannel daemon set is restarted to reflect the new Windows-specific configuration + & kubectl apply -f "..\kube-flannel.yaml" >> logs + & kubectl rollout restart ds kube-flannel-ds -n kube-flannel >> logs + & kubectl get pods -A >> logs + Write-Output "* Flannel CNI for Windows is configured and the daemon set is restarted ..." + + + Enter-PSSession -VMName $VMName -Credential $Credential + + # Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock {Get-Culture} + + $CurrrentDirectory = $PWD + $LocalScriptsPath = Split-Path -Path $CurrrentDirectory -Parent + $CompressedFilePath = "$LocalScriptsPath\MinikubeWindowsContainers.zip" + + # 'auto-install.iso' is not being compressed since another process will be using it + # hack way of compressing the error but it works, needs exploration + Compress-Archive -Path $LocalScriptsPath -DestinationPath $CompressedFilePath -Force 2>$null + + $RemoteScriptsPath = "C:\Users\Administrator\Documents" + + $session = New-PSSession -VMName $VMName -Credential $Credential + + Copy-Item -Path $CompressedFilePath -Destination $RemoteScriptsPath -Force -ToSession $Session + + $ScriptBlock = { + $CompressedFilePath = "C:\Users\Administrator\Documents\MinikubeWindowsContainers.zip" + $UncompressedFolderPath = "C:\Users\Administrator\Documents" + Expand-Archive -Path $CompressedFilePath -DestinationPath $UncompressedFolderPath -Force + } + + Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock $ScriptBlock + + $ScriptBlock = { + $UncompressedFolderPath = "C:\Users\Administrator\Documents\MinikubeWindowsContainers" + Import-Module -Name "$UncompressedFolderPath\automation\ContainerdTools.psm1" -Force + + Install-Containerd + Initialize-ContainerdService + Start-ContainerdService + + Exit-PSSession + } + Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock $ScriptBlock + + $ScriptBlock = { + $UncompressedFolderPath = "C:\Users\Administrator\Documents\MinikubeWindowsContainers" + Import-Module -Name "$UncompressedFolderPath\automation\NSSMTools.psm1" -Force + + Install-NSSM + + Exit-PSSession + } + Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock $ScriptBlock + + $ScriptBlock = { + [CmdletBinding()] + param ( + [Parameter()] + [string] + $KubernetesVersion + ) + $UncompressedFolderPath = "C:\Users\Administrator\Documents\MinikubeWindowsContainers" + Import-Module -Name "$UncompressedFolderPath\automation\k8Tools.psm1" -Force + + Install-Kubelet -KubernetesVersion $KubernetesVersion + Set-Port + + Exit-PSSession + } + Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $KubernetesVersion + + + $commandString = "minikube ip" + $IP = Invoke-Expression -Command $commandString + + $ScriptBlock = { + [CmdletBinding()] + param ( + [Parameter()] + [string] + $IP + ) + $UncompressedFolderPath = "C:\Users\Administrator\Documents\MinikubeWindowsContainers" + + Import-Module -Name "$UncompressedFolderPath\automation\MinikubeTools.psm1" -Force + + Add-Host -IP $IP + + Exit-PSSession + } + + Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $IP + + + $JoinCommand = Get-JoinCommand -KubernetesVersion $KubernetesVersion + + $ScriptBlock = { + [CmdletBinding()] + param ( + [Parameter()] + [string] + $JoinCommand, + + [Parameter()] + [string] + $KubernetesVersion + ) + $UncompressedFolderPath = "C:\Users\Administrator\Documents\MinikubeWindowsContainers" + + Import-Module -Name "$UncompressedFolderPath\automation\MinikubeTools.psm1" -Force + Import-Module -Name "$UncompressedFolderPath\automation\k8Tools.psm1" -Force + + + Get-Kubeadm -KubernetesVersion $KubernetesVersion + + + Set-Location -Path "C:\k" + + Write-Output "* Joining the Windows node to the cluster ..." + + Invoke-Expression "$JoinCommand >> logs 2>&1" + + Set-MinikubeFolderError + + Invoke-Expression "$JoinCommand >> logs 2>&1" + + Exit-PSSession + } + + Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $JoinCommand, $KubernetesVersion + + # configure flannel and kube-proxy on the windows node + & kubectl apply -f "..\flannel-overlay.yaml" >> logs + & kubectl apply -f "..\kube-proxy.yaml" >> logs + + Write-Output "* Waiting for the node to come to a ready state ..." + Start-Sleep -Seconds 40 + + # validate windows node successfully join + & kubectl get nodes -o wide >> logs + + # check the status of the windows node + & kubectl get nodes -o wide + Write-Output "* Windows node is successfully joined and configured ..." + +} \ No newline at end of file diff --git a/automation/SetUp.ps1 b/automation/SetUp.ps1 new file mode 100644 index 0000000..977e461 --- /dev/null +++ b/automation/SetUp.ps1 @@ -0,0 +1,112 @@ +param( + [string]$SwitchName = "Default Switch", + + [Parameter(Mandatory=$true)] + [string]$ISOFilePath, + + [Parameter(Mandatory=$true)] + [string]$VMName, + + [Parameter(Mandatory=$true)] + [string]$UserName, + + [Parameter(Mandatory=$true)] + [string]$Pass, + + [string]$KubernetesVersion +) + +Import-Module -Name "$PSScriptRoot\k8Tools.psm1" -Force + + +if ([string]::IsNullOrEmpty($KubernetesVersion)) { + $KubernetesVersion = Get-k8LatestVersion + Write-Output "* The latest Kubernetes version is $KubernetesVersion" + $KubernetesVersion = $KubernetesVersion.TrimStart('v') +} + + +"* Starting the $VMName Virtual Machine ..." > logs +Write-Output "* Starting the $VMName Virtual Machine ..." + +$VM = @{ + Name = $VMName; + Generation = 1; + MemoryStartupBytes = 1GB; + NewVHDPath = "${env:homepath}\.minikube\machines\$VMName\VHD.vhdx"; + NewVHDSizeBytes = 15GB; + BootDevice = "VHD"; + Path = "${env:homepath}\.minikube\machines\"; + SwitchName = $SwitchName +} + +Write-Output "* Please wait as we set up the $VMName Virtual Machine ..." +New-VM @VM | Out-Null +Set-VM -Name $VMName -ProcessorCount 2 -AutomaticCheckpointsEnabled $false +Set-VMProcessor -VMName $VMName -ExposeVirtualizationExtensions $true +Set-VMDvdDrive -VMName $VMName -Path $ISOFilePath +Add-VMDvdDrive -VMName $VMName -Path "..\auto-install.iso" -ControllerNumber 1 -ControllerLocation 1 +Start-VM -Name $VMName | Out-Null + + + +$timeout = 600 +$retryInterval = 15 +$elapsedTime = 0 + +do { + Start-Sleep -Seconds $retryInterval + "Waiting for the VM to start ..." >> logs + $heartbeat = Get-VMIntegrationService -VMName $VMName -Name "Heartbeat" + $elapsedTime += $retryInterval + + if ($elapsedTime -ge $timeout) { + Write-Output "* Timeout reached. Unable to start the VM ..." + Write-Output "* Exiting the script ..." + "Timeout reached. Exiting the script ..." >> logs + "Exiting the script ..." >> logs + exit + } +} while ($heartbeat.PrimaryStatusDescription -ne "OK") + +Write-Output "* The $VMName Virtual Machine is started ..." + + +$SecurePassword = ConvertTo-SecureString -String $Pass -AsPlainText -Force +$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $SecurePassword + +$VMStatus = Get-VM -Name $VMName | Select-Object -ExpandProperty State + +if ($VMStatus -eq 'Running') { + + "The $VMName Virtual Machine is running" >> logs + + $retryInterval = 45 + $timeout = 120 + $elapsedTime = 0 + + do { + + try { + $os = Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock { Get-WmiObject -Query "SELECT * FROM Win32_OperatingSystem" } -ErrorAction Stop + + if ($os) { + Write-Output "* Windows is successfully installed on $VMName" + "Windows is successfully installed on $VMName" >> logs + . .\Run.ps1 + # . "$PSScriptRoot\Run.ps1" === this also works + RUN -VMName $VMName -UserName $UserName -Pass $Pass -Credential $Credential -KubernetesVersion $KubernetesVersion + break + } else { + Write-Output "* Windows is not installed on $VMName" + } + } catch { + Write-Output "* An error occurred while checking if Windows is installed on ${VMName}: $_" + } + Start-Sleep -Seconds $retryInterval + $elapsedTime += $retryInterval + } while ($elapsedTime -lt $timeout) + +} else { + Write-Output "The VM $VMName is not running" +} diff --git a/automation/SetUpUtilities.psm1 b/automation/SetUpUtilities.psm1 new file mode 100644 index 0000000..b9b30a6 --- /dev/null +++ b/automation/SetUpUtilities.psm1 @@ -0,0 +1,161 @@ +$envPathRegKey = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" + +function Get-LatestToolVersion($repository) { + try { + $uri = "https://api.github.com/repos/$repository/releases/latest" + $response = Invoke-WebRequest -Uri $uri -UseBasicParsing + $version = ($response.content | ConvertFrom-Json).tag_name + return $version.TrimStart("v") + } + catch { + Throw "Could not get $repository version. $_" + } +} + +function ParsePathString($pathString) { + $parsedString = $pathString -split ";" | ` + ForEach-Object { $_.TrimEnd("\") } | ` + Select-Object -Unique | ` + Where-Object { ![string]::IsNullOrWhiteSpace($_) } + + if (!$parsedString) { + $DebugPreference = 'Stop' + Write-Debug "Env path cannot be null or an empty string" + } + return $parsedString -join ";" +} + +function Install-RequiredFeature { + param( + [string] $Feature, + [string] $InstallPath, + [string] $DownloadPath, + [string] $EnvPath, + [boolean] $cleanup + ) + + # Create the directory to untar to + Write-Information -InformationAction Continue -MessageData "* Extracting $Feature to $InstallPath ..." + if (!(Test-Path $InstallPath)) { + New-Item -ItemType Directory -Force -Path $InstallPath | Out-Null + } + + # Untar file + if ($DownloadPath.EndsWith("tar.gz")) { + tar.exe -xf $DownloadPath -C $InstallPath + if ($LASTEXITCODE -gt 0) { + Throw "Could not untar $DownloadPath. $_" + } + } + + # Add to env path + Add-FeatureToPath -Feature $Feature -Path $EnvPath + + # Clean up + if ($CleanUp) { + Write-Output "Cleanup to remove downloaded files" + Remove-Item $downloadPath -Force -ErrorAction Continue + } +} + +function Uninstall-ContainerTool ($tool, $path) { + $pathItems = Get-ChildItem -Path $path -ErrorAction SilentlyContinue + if ($null -eq $pathItems) { + return + } + + Write-Warning "Uninstalling preinstalled $tool at the path $path" + try { + $command = "Uninstall-$tool -Path '$path'" + Invoke-Expression -Command $command + } + catch { + Throw "Could not uninstall $tool. $_" + } +} + +function Add-FeatureToPath { + param ( + [string] + [ValidateNotNullOrEmpty()] + [parameter(HelpMessage = "Feature to add to env path")] + $feature, + + [string] + [ValidateNotNullOrEmpty()] + [parameter(HelpMessage = "Path where the feature is installed")] + $path + ) + + $currPath = (Get-ItemProperty -Path $envPathRegKey -Name path).path + $currPath = ParsePathString -PathString $currPath + if (!($currPath -like "*$feature*")) { + # Write-Information -InformationAction Continue -MessageData "Adding $feature to Environment Path RegKey" + + # Add to reg key + Set-ItemProperty -Path $envPathRegKey -Name PATH -Value "$currPath;$path" + } + + $currPath = ParsePathString -PathString $env:Path + if (!($currPath -like "*$feature*")) { + # Write-Information -InformationAction Continue -MessageData "Adding $feature to env path" + # Add to env path + [Environment]::SetEnvironmentVariable("Path", "$($env:path);$path", [System.EnvironmentVariableTarget]::Machine) + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + } +} + +function Remove-FeatureFromPath { + param ( + [string] + [ValidateNotNullOrEmpty()] + [parameter(HelpMessage = "Feature to remove from env path")] + $feature + ) + + # Remove from regkey + $currPath = (Get-ItemProperty -Path $envPathRegKey -Name path).path + $currPath = ParsePathString -PathString $currPath + if ($currPath -like "*$feature*") { + $NewPath = removeFeatureFromPath -PathString $currPath -Feature $feature + Set-ItemProperty -Path $envPathRegKey -Name PATH -Value $NewPath + } + + # Remove from env path + $currPath = ParsePathString -PathString $env:Path + if ($currPath -like "*$feature*") { + Write-Information -InformationAction Continue -MessageData "Removing $feature from env path" + $newPathString = removeFeatureFromPath -PathString $currPath -Feature $feature + [Environment]::SetEnvironmentVariable("Path", "$newPathString", [System.EnvironmentVariableTarget]::Machine) + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + } +} + +function ParsePathString($pathString) { + $parsedString = $pathString -split ";" | ` + ForEach-Object { $_.TrimEnd("\") } | ` + Select-Object -Unique | ` + Where-Object { ![string]::IsNullOrWhiteSpace($_) } + + if (!$parsedString) { + $DebugPreference = 'Stop' + Write-Debug "Env path cannot be null or an empty string" + } + return $parsedString -join ";" +} + +function RemoveFeatureFromPath ($pathString, $feature) { + $parsedString = $pathString -split ";" | Where-Object { !($_ -like "*$feature*") } + + if (!$parsedString) { + $DebugPreference = 'Stop' + Write-Debug "Env path cannot be null or an empty string" + } + return $parsedString -join ";" +} + +Export-ModuleMember -Function Get-LatestToolVersion +Export-ModuleMember -Function Install-RequiredFeature +Export-ModuleMember -Function Uninstall-ContainerTool +Export-ModuleMember -Function Add-FeatureToPath +Export-ModuleMember -Function Remove-FeatureFromPath \ No newline at end of file diff --git a/automation/autounattend.xml b/automation/autounattend.xml new file mode 100644 index 0000000..67acd53 --- /dev/null +++ b/automation/autounattend.xml @@ -0,0 +1,214 @@ + + + + + + en-US + + en-US + en-US + en-US + en-US + + + + + + + 250 + 1 + Primary + + + 2 + true + Primary + + + + + 1 + 1 + NTFS + + true + + + 2 + 2 + NTFS + + + + 0 + true + + + + + + + + + /IMAGE/INDEX + 3 + + + + 0 + 2 + + OnError + false + + + + true + + Never + + + + + + + + + + + en-US + en-US + en-US + en-US + en-US + + + + minikube-ws22 + Central Standard Time + true + + + + true + + + + + + + + Minikube@2024 + true</PlainText> + </Password> + <Username>Administrator</Username> + <Enabled>true</Enabled> + </AutoLogon> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName "WinRMCertificate"</CommandLine> + <Description>Certificate for WinRM</Description> + <Order>1</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command Enable-PSRemoting -SkipNetworkProfileCheck -Force</CommandLine> + <Description>Enable WinRM</Description> + <Order>2</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command ($cert = gci Cert:\LocalMachine\My\) -and (New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $cert.Thumbprint –Force)</CommandLine> + <Description>Add HTTPS WinRM listener with previously generated certificate</Description> + <Order>3</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command New-NetFirewallRule -DisplayName 'Windows Remote Management (HTTPS-In)' -Name 'Windows Remote Management (HTTPS-In)' -Profile Any -LocalPort 5986 -Protocol TCP</CommandLine> + <Description>Add firewall exception to TCP port 5986 for WinRM over HTTPS</Description> + <Order>4</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true</CommandLine> + <Description>Enable Basic authentication</Description> + <Order>5</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command Stop-Service WinRM</CommandLine> + <Description>Stop the WinRM service to allow the dism process to finish before packer executes scripts</Description> + <Order>6</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command Install-WindowsFeature -Name containers</CommandLine> + <Order>7</Order> + <Description>Installs Containers feature</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command Set-SConfig -AutoLaunch $false</CommandLine> + <Order>8</Order> + <Description>Turns off Server Configuration tool (SConfig)</Description> + </SynchronousCommand> + + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command Restart-Computer -Force</CommandLine> + <Order>9</Order> + <Description>Restart computer to apply changes</Description> + </SynchronousCommand> + + <!-- for some weird reason this only seem to work if I have it after the restart bit--> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0</CommandLine> + <Description>Install the OpenSSH Client</Description> + <Order>10</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0</CommandLine> + <Description>Install the OpenSSH Server</Description> + <Order>11</Order> + </SynchronousCommand> + + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command Start-Service sshd</CommandLine> + <Description>Start SSH Service</Description> + <Order>12</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command Set-Service -Name sshd -StartupType 'Automatic'</CommandLine> + <Description>Set SSH Service to Automatic</Description> + <Order>13</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22</CommandLine> + <Description>Create Firewall Rule for OpenSSH-Server-In-TCP</Description> + <Order>14</Order> + </SynchronousCommand> + + + </FirstLogonCommands> + <UserAccounts> + <AdministratorPassword> + <Value>Minikube@2024</Value> + <PlainText>true</PlainText> + </AdministratorPassword> + </UserAccounts> + </component> + </settings> + <cpi:offlineImage cpi:source="wim:c:/wims/install.wim#Windows Server 2022 SERVERDATACENTER" + xmlns:cpi="urn:schemas-microsoft-com:cpi" /> + </unattend> \ No newline at end of file diff --git a/automation/k8Tools.psm1 b/automation/k8Tools.psm1 new file mode 100644 index 0000000..c7f214a --- /dev/null +++ b/automation/k8Tools.psm1 @@ -0,0 +1,94 @@ +Import-Module -Name "$PSScriptRoot\SetUpUtilities.psm1" -Force + +function Get-k8LatestVersion { + $latestVersion = Get-LatestToolVersion -Repository "kubernetes/kubernetes" + return $latestVersion +} + +function Install-Kubelet { + param ( + [string] + $KubernetesVersion + ) + + # Check if kubelet service is already installed + $nssmService = Get-WmiObject win32_service | Where-Object {$_.PathName -like '*nssm*'} + if ($nssmService.Name -eq 'kubelet') { + Write-Output "Kubelet service is already installed." + return + } + + # Define the URL for kubelet download + $KubeletUrl = "https://dl.k8s.io/v$KubernetesVersion/bin/windows/amd64/kubelet.exe" + + # Download kubelet + try { + Invoke-WebRequest -Uri $KubeletUrl -OutFile "c:\k\kubelet.exe" | Out-Null + } catch { + Write-Error "Failed to download kubelet: $_" + } + + # Create the Start-kubelet.ps1 script + @" +`$FileContent = Get-Content -Path "/var/lib/kubelet/kubeadm-flags.env" +`$kubeAdmArgs = `$FileContent.TrimStart(`'KUBELET_KUBEADM_ARGS=`').Trim(`'"`') + +`$args = "--cert-dir=`$env:SYSTEMDRIVE/var/lib/kubelet/pki", + "--config=`$env:SYSTEMDRIVE/var/lib/kubelet/config.yaml", + "--bootstrap-kubeconfig=`$env:SYSTEMDRIVE/etc/kubernetes/bootstrap-kubelet.conf", + "--kubeconfig=`$env:SYSTEMDRIVE/etc/kubernetes/kubelet.conf", + "--hostname-override=`$(hostname)", + "--enable-debugging-handlers", + "--cgroups-per-qos=false", + "--enforce-node-allocatable=``"``"", + "--resolv-conf=``"``"" + +`$kubeletCommandLine = "c:\k\kubelet.exe " + (`$args -join " ") + " `$kubeAdmArgs" +Invoke-Expression `$kubeletCommandLine +"@ | Set-Content -Path "c:\k\Start-kubelet.ps1" + + # Install kubelet as a Windows service + c:\k\nssm.exe install kubelet Powershell -ExecutionPolicy Bypass -NoProfile c:\k\Start-kubelet.ps1 | Out-Null + c:\k\nssm.exe set Kubelet AppStdout C:\k\kubelet.log | Out-Null + c:\k\nssm.exe set Kubelet AppStderr C:\k\kubelet.err.log | Out-Null + + Write-Output "* Kubelet is installed and the service is started ..." +} + +function Set-Port { + $firewallRule = Get-NetFirewallRule -Name 'kubelet' -ErrorAction SilentlyContinue + if ($firewallRule) { + Write-Output "Firewall rule 'kubelet' already exists." + return + } + + $ruleParams = @{ + Name = 'kubelet' + DisplayName = 'kubelet' + Enabled = "True" + Direction = 'Inbound' + Protocol = 'TCP' + Action = 'Allow' + LocalPort = 10250 + } + + New-NetFirewallRule @ruleParams | Out-Null +} + +function Get-Kubeadm { + param ( + [string] + $KubernetesVersion + ) + try { + Invoke-WebRequest -Uri "https://dl.k8s.io/v$KubernetesVersion/bin/windows/amd64/kubeadm.exe" -OutFile "c:\k\kubeadm.exe" | Out-Null + } catch { + Write-Error "Failed to download kubeadm: $_" + } +} + +Export-ModuleMember -Function Get-k8LatestVersion +Export-ModuleMember -Function Install-Kubelet +Export-ModuleMember -Function Set-Port +Export-ModuleMember -Function Get-Kubeadm +Export-ModuleMember -Function Get-k8LatestVersion