@@ -191,7 +191,7 @@ func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
191191 Config : config ,
192192 ConfigOwner : configOwner ,
193193 Cluster : cluster ,
194- provisioner : getProvisioner (config .Spec .Ignition ),
194+ provisioner : getProvisioner (config .Spec .Ignition , config . Spec . IsWindows ),
195195 }
196196 err = r .setClientScope (ctx , cluster , scope )
197197 if err != nil {
@@ -262,11 +262,156 @@ func (r *Controller) generateBootstrapDataForWorker(ctx context.Context, log log
262262 files = append (files , resolveCertsForIngress ... )
263263 }
264264
265+ commandsMap := make (map [provisioner.VarName ]string )
266+ var commands []string
267+
268+ if scope .Config .Spec .IsWindows {
269+ var winFiles []provisioner.File
270+ commands , winFiles = getWindowsCommands (scope )
271+ files = append (files , winFiles ... )
272+ } else {
273+ commands , commandsMap , err = getLinuxCommands (scope )
274+ if err != nil {
275+ return nil , fmt .Errorf ("error generating linux commands: %w" , err )
276+ }
277+ }
278+
279+ var (
280+ customUserData string
281+ vars map [provisioner.VarName ]string
282+ )
283+ if scope .Config .Spec .CustomUserDataRef != nil {
284+ customUserData , err = resolveContentFromFile (ctx , r .Client , scope .Cluster , scope .Config .Spec .CustomUserDataRef )
285+ if err != nil {
286+ return nil , fmt .Errorf ("error extracting the contents of the provided custom worker user data: %w" , err )
287+ }
288+ vars = commandsMap
289+ }
290+
291+ return scope .provisioner .ToProvisionData (& provisioner.InputProvisionData {
292+ Files : files ,
293+ Commands : commands ,
294+ CustomUserData : customUserData ,
295+ Vars : vars ,
296+ })
297+ }
298+
299+ func getWindowsCommands (scope * Scope ) ([]string , []provisioner.File ) {
300+ //if scope.Config.Spec.K0sInstallDir == "/usr/local/bin" {
301+ // scope.Config.Spec.K0sInstallDir = "C:\\bootstrap"
302+ //}
303+ k0sPath := filepath .Join (scope .Config .Spec .K0sInstallDir , "k0s.exe" )
304+
305+ installScript := fmt .Sprintf (`$ErrorActionPreference = "Stop"
306+ Start-Transcript -Path C:\bootstrap.log -Append
307+
308+ if (Test-Path C:\bootstrap.done) {
309+ Write-Host "Bootstrap already completed, skipping."
310+ exit 0
311+ }
312+
313+ Write-Host "=== Register ScheduledTask to continue execution after reboot ==="
314+
315+ $taskName = "k0s-bootstrap"
316+
317+ if (-not (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue)) {
318+ $action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\bootstrap\k0s_install.ps1"
319+ $trigger = New-ScheduledTaskTrigger -AtStartup
320+ $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
321+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 2)
322+
323+ Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
324+ }
325+
326+ Write-Host "=== Checking Windows Containers feature using DISM ==="
327+
328+ function Is-FeatureEnabled {
329+ param([string]$Name)
330+ $output = dism.exe /online /Get-Features
331+ return $output -match "$Name\s+:\s+Enabled"
332+ }
333+
334+ $featureName = "Containers"
335+
336+ if (Is-FeatureEnabled $featureName) {
337+ Write-Host "$featureName is already enabled. Nothing to do."
338+ } else {
339+ Write-Host "$featureName is not enabled. Installing..."
340+ $result = dism.exe /online /Enable-Feature /FeatureName:$featureName /All /NoRestart
341+ $exitCode = $LASTEXITCODE
342+ Write-Host "DISM exit code: $exitCode"
343+
344+ if ($exitCode -eq 0) {
345+ Write-Host "Feature installed successfully, no reboot required."
346+ } elseif ($exitCode -eq 3010) {
347+ Write-Host "Feature installed successfully, reboot required."
348+ Restart-Computer -Force
349+ } else {
350+ throw "DISM failed with exit code $exitCode"
351+ }
352+ }
353+
354+ # --- "Waiting for network..." ---
355+
356+ while (-not (Get-NetAdapter | Where-Object Status -eq 'Up')) {
357+ Start-Sleep -Seconds 5
358+ }
359+
360+ Write-Host "Network is up"
361+
362+ # --- Download and run k0s ---
363+ Write-Host "=== Downloading k0s binary ==="
364+
365+ $k0sUrl = "https://get.k0sproject.io/%s/k0s-%s-amd64.exe" # PoC supporting only amd64
366+ $dest = "%s"
367+ New-Item -ItemType Directory -Force -Path "%s" | Out-Null
368+ Invoke-WebRequest -Uri $k0sUrl -OutFile $dest -UseBasicParsing
369+
370+ Write-Host "=== Executing k0s to check version ==="
371+ & $dest --version
372+ ` , scope .Config .Spec .Version , scope .Config .Spec .Version , k0sPath , scope .Config .Spec .K0sInstallDir )
373+
374+ inlineCommands := scope .Config .Spec .PreStartCommands
375+ // Download and enable containers and k0s bootstrap script
376+ commands := []string {`powershell.exe -NoProfile -NonInteractive -File "C:\bootstrap\k0s_install.ps1"` }
377+ // TODO: implement ingress support for Windows
378+ //inlineCommands = append(inlineCommands, ingressCommands...)
379+
380+ installCmdParts := []string {
381+ fmt .Sprintf (`%s install worker --token-file %s` , k0sPath , scope .Config .GetJoinTokenPath ()),
382+ }
383+ installCmdParts = append (installCmdParts , mergeExtraArgs (scope .Config .Spec .Args , scope .ConfigOwner , true , scope .Config .Spec .UseSystemHostname )... )
384+
385+ inlineCommands = append (inlineCommands , strings .Join (installCmdParts , " " ))
386+ inlineCommands = append (inlineCommands , fmt .Sprintf (`& %s start` , k0sPath ))
387+ inlineCommands = append (inlineCommands , scope .Config .Spec .PostStartCommands ... )
388+
389+ for _ , cmd := range inlineCommands {
390+ installScript += "\n " + cmd + "\n "
391+ }
392+
393+ installScript += `
394+ Unregister-ScheduledTask -TaskName "k0s-bootstrap" -Confirm:$false -ErrorAction SilentlyContinue
395+ New-Item C:\bootstrap.done -ItemType File
396+ Stop-Transcript
397+ `
398+
399+ var files []provisioner.File
400+ files = append (files , provisioner.File {
401+ Path : `C:\bootstrap\k0s_install.ps1` ,
402+ Permissions : "0644" ,
403+ Content : installScript ,
404+ })
405+
406+ return commands , files
407+ }
408+
409+ func getLinuxCommands (scope * Scope ) ([]string , map [provisioner.VarName ]string , error ) {
265410 commandsMap := make (map [provisioner.VarName ]string )
266411
267412 downloadCommands , err := util .DownloadCommands (scope .Config .Spec .PreInstalledK0s , scope .Config .Spec .DownloadURL , scope .Config .Spec .Version , scope .Config .Spec .K0sInstallDir )
268413 if err != nil {
269- return nil , fmt .Errorf ("error generating download commands: %w" , err )
414+ return nil , nil , fmt .Errorf ("error generating download commands: %w" , err )
270415 }
271416 installCmd := createInstallCmd (scope )
272417
@@ -288,24 +433,7 @@ func (r *Controller) generateBootstrapDataForWorker(ctx context.Context, log log
288433 // https://cluster-api.sigs.k8s.io/developer/providers/contracts/bootstrap-config#sentinel-file
289434 commands = append (commands , "mkdir -p /run/cluster-api && touch /run/cluster-api/bootstrap-success.complete" )
290435
291- var (
292- customUserData string
293- vars map [provisioner.VarName ]string
294- )
295- if scope .Config .Spec .CustomUserDataRef != nil {
296- customUserData , err = resolveContentFromFile (ctx , r .Client , scope .Cluster , scope .Config .Spec .CustomUserDataRef )
297- if err != nil {
298- return nil , fmt .Errorf ("error extracting the contents of the provided custom worker user data: %w" , err )
299- }
300- vars = commandsMap
301- }
302-
303- return scope .provisioner .ToProvisionData (& provisioner.InputProvisionData {
304- Files : files ,
305- Commands : commands ,
306- CustomUserData : customUserData ,
307- Vars : vars ,
308- })
436+ return commands , commandsMap , nil
309437}
310438
311439func (r * Controller ) getK0sToken (ctx context.Context , scope * Scope ) (string , error ) {
0 commit comments