Skip to content

Commit 049ba8c

Browse files
committed
Windows support PoC
Signed-off-by: Alexey Makhov <amakhov@mirantis.com> Signed-off-by: makhov <amakhov@mirantis.com>
1 parent c42cb32 commit 049ba8c

14 files changed

Lines changed: 441 additions & 26 deletions

api/bootstrap/v1beta1/k0s_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ type K0sWorkerConfigSpec struct {
122122

123123
// WorkingDir specifies the working directory where k0smotron will place its files.
124124
WorkingDir string `json:"workingDir,omitempty"`
125+
126+
// IsWindows specifies whether the target node is Windows.
127+
// +kubebuilder:validation:Optional
128+
IsWindows bool `json:"isWindows,omitempty"`
125129
}
126130

127131
// SecretMetadata defines metadata to be propagated to the bootstrap Secret

config/clusterapi/bootstrap/bases/bootstrap.cluster.x-k8s.io_k0sworkerconfigs.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ spec:
170170
- variant
171171
- version
172172
type: object
173+
isWindows:
174+
description: IsWindows specifies whether the target node is Windows.
175+
type: boolean
173176
k0sInstallDir:
174177
default: /usr/local/bin
175178
description: |-

config/clusterapi/bootstrap/bases/bootstrap.cluster.x-k8s.io_k0sworkerconfigtemplates.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ spec:
194194
- variant
195195
- version
196196
type: object
197+
isWindows:
198+
description: IsWindows specifies whether the target node is
199+
Windows.
200+
type: boolean
197201
k0sInstallDir:
198202
default: /usr/local/bin
199203
description: |-

config/crd/bases/bootstrap/bootstrap.cluster.x-k8s.io_k0sworkerconfigs.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ spec:
170170
- variant
171171
- version
172172
type: object
173+
isWindows:
174+
description: IsWindows specifies whether the target node is Windows.
175+
type: boolean
173176
k0sInstallDir:
174177
default: /usr/local/bin
175178
description: |-

config/crd/bases/bootstrap/bootstrap.cluster.x-k8s.io_k0sworkerconfigtemplates.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ spec:
194194
- variant
195195
- version
196196
type: object
197+
isWindows:
198+
description: IsWindows specifies whether the target node is
199+
Windows.
200+
type: boolean
197201
k0sInstallDir:
198202
default: /usr/local/bin
199203
description: |-

docs/resource-reference/bootstrap.cluster.x-k8s.io-v1beta1.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,13 @@ If specified the version field is ignored and what ever version is downloaded fr
804804
Ignition defines the ignition configuration. If empty, k0smotron will use cloud-init.<br/>
805805
</td>
806806
<td>false</td>
807+
</tr><tr>
808+
<td><b>isWindows</b></td>
809+
<td>boolean</td>
810+
<td>
811+
IsWindows specifies whether the target node is Windows.<br/>
812+
</td>
813+
<td>false</td>
807814
</tr><tr>
808815
<td><b>k0sInstallDir</b></td>
809816
<td>string</td>
@@ -1541,6 +1548,13 @@ If specified the version field is ignored and what ever version is downloaded fr
15411548
Ignition defines the ignition configuration. If empty, k0smotron will use cloud-init.<br/>
15421549
</td>
15431550
<td>false</td>
1551+
</tr><tr>
1552+
<td><b>isWindows</b></td>
1553+
<td>boolean</td>
1554+
<td>
1555+
IsWindows specifies whether the target node is Windows.<br/>
1556+
</td>
1557+
<td>false</td>
15441558
</tr><tr>
15451559
<td><b>k0sInstallDir</b></td>
15461560
<td>string</td>

internal/controller/bootstrap/common.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ func mergeExtraArgs(configArgs []string, configOwner *bsutil.ConfigOwner, isWork
103103
return args
104104
}
105105

106-
func getProvisioner(ignitionSpec *bootstrapv1.IgnitionSpec) provisioner.Provisioner {
106+
func getProvisioner(ignitionSpec *bootstrapv1.IgnitionSpec, isWindows bool) provisioner.Provisioner {
107+
if isWindows {
108+
return &provisioner.PowerShellAWSProvisioner{}
109+
}
110+
107111
if ignitionSpec != nil {
108112
return &provisioner.IgnitionProvisioner{
109113
Variant: ignitionSpec.Variant,

internal/controller/bootstrap/controlplane_bootstrap_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func (c *ControlPlaneController) Reconcile(ctx context.Context, req ctrl.Request
171171
Cluster: cluster,
172172
WorkerEnabled: false,
173173
currentKCPVersion: currentKCPVersion,
174-
provisioner: getProvisioner(config.Spec.Ignition),
174+
provisioner: getProvisioner(config.Spec.Ignition, false),
175175
installArgs: append([]string{}, config.Spec.Args...),
176176
}
177177

internal/controller/bootstrap/worker_bootstrap_controller.go

Lines changed: 148 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

311439
func (r *Controller) getK0sToken(ctx context.Context, scope *Scope) (string, error) {

internal/controller/bootstrap/worker_bootstrap_controller_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,37 @@ func Test_createInstallCmd(t *testing.T) {
126126
}
127127
}
128128

129+
func Test_getWindowsCommands(t *testing.T) {
130+
tests := []struct {
131+
name string
132+
scope *Scope
133+
want []string
134+
}{
135+
{
136+
name: "with default config",
137+
scope: &Scope{
138+
Config: &bootstrapv1.K0sWorkerConfig{
139+
Spec: bootstrapv1.K0sWorkerConfigSpec{
140+
IsWindows: true,
141+
},
142+
},
143+
ConfigOwner: &bsutil.ConfigOwner{Unstructured: &unstructured.Unstructured{Object: map[string]interface{}{}}},
144+
},
145+
want: []string{
146+
"powershell.exe -NoProfile -NonInteractive -File \"C:\\bootstrap\\k0s_install.ps1\"",
147+
},
148+
},
149+
}
150+
151+
for _, tt := range tests {
152+
t.Run(tt.name, func(t *testing.T) {
153+
got, _ := getWindowsCommands(tt.scope)
154+
require.Equal(t, tt.want, got)
155+
})
156+
}
157+
158+
}
159+
129160
func Test_createBootstrapSecret(t *testing.T) {
130161
tests := []struct {
131162
name string

0 commit comments

Comments
 (0)