Skip to content

Commit 0bde38c

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 0bde38c

14 files changed

+444
-26
lines changed

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: 106 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,114 @@ 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+
var files []provisioner.File
306+
files = append(files, provisioner.File{
307+
Path: `C:\bootstrap\k0s_install.ps1`,
308+
Permissions: "0644",
309+
Content: fmt.Sprintf(`Write-Host "=== Checking Windows Containers feature using DISM ==="
310+
311+
function Is-FeatureEnabled {
312+
param([string]$Name)
313+
$output = dism.exe /online /Get-Features
314+
return $output -match "$Name\s+:\s+Enabled"
315+
}
316+
317+
$featureName = "Containers"
318+
319+
if (Is-FeatureEnabled $featureName) {
320+
Write-Host "$featureName is already enabled. Nothing to do."
321+
} else {
322+
Write-Host "$featureName is not enabled. Installing..."
323+
$result = dism.exe /online /Enable-Feature /FeatureName:$featureName /All /NoRestart
324+
$exitCode = $LASTEXITCODE
325+
Write-Host "DISM exit code: $exitCode"
326+
327+
if ($exitCode -eq 0) {
328+
Write-Host "Feature installed successfully, no reboot required."
329+
} elseif ($exitCode -eq 3010) {
330+
Write-Host "Feature installed successfully, reboot required."
331+
Restart-Computer -Force
332+
} else {
333+
throw "DISM failed with exit code $exitCode"
334+
}
335+
}
336+
337+
# --- Download and run k0s ---
338+
Write-Host "=== Downloading k0s binary ==="
339+
340+
$k0sUrl = "https://get.k0sproject.io/%s/k0s-%s-amd64.exe" # PoC supporting only amd64
341+
$dest = "%s"
342+
New-Item -ItemType Directory -Force -Path "%s" | Out-Null
343+
Invoke-WebRequest -Uri $k0sUrl -OutFile $dest -UseBasicParsing
344+
345+
Write-Host "=== Executing k0s to check version ==="
346+
& $dest --version`, scope.Config.Spec.Version, scope.Config.Spec.Version, k0sPath, scope.Config.Spec.K0sInstallDir),
347+
})
348+
349+
commands := scope.Config.Spec.PreStartCommands
350+
// Download and enable containers and k0s bootstrap script
351+
commands = append(commands, `powershell.exe -NoProfile -NonInteractive -File "C:\bootstrap\k0s_install.ps1"`)
352+
// TODO: implement ingress support for Windows
353+
//commands = append(commands, ingressCommands...)
354+
355+
installCmdParts := []string{
356+
fmt.Sprintf(`%s install worker --token-file %s`, k0sPath, scope.Config.GetJoinTokenPath()),
357+
}
358+
installCmdParts = append(installCmdParts, mergeExtraArgs(scope.Config.Spec.Args, scope.ConfigOwner, true, scope.Config.Spec.UseSystemHostname)...)
359+
360+
commands = append(commands, `powershell.exe -NoProfile -NonInteractive -Command "& { `+strings.Join(installCmdParts, " ")+` }"`)
361+
commands = append(commands, fmt.Sprintf(`powershell.exe -NoProfile -NonInteractive -Command "& { %s start }"`, k0sPath))
362+
commands = append(commands, scope.Config.Spec.PostStartCommands...)
363+
364+
return commands, files
365+
}
366+
367+
func getLinuxCommands(scope *Scope) ([]string, map[provisioner.VarName]string, error) {
265368
commandsMap := make(map[provisioner.VarName]string)
266369

267370
downloadCommands, err := util.DownloadCommands(scope.Config.Spec.PreInstalledK0s, scope.Config.Spec.DownloadURL, scope.Config.Spec.Version, scope.Config.Spec.K0sInstallDir)
268371
if err != nil {
269-
return nil, fmt.Errorf("error generating download commands: %w", err)
372+
return nil, nil, fmt.Errorf("error generating download commands: %w", err)
270373
}
271374
installCmd := createInstallCmd(scope)
272375

@@ -288,24 +391,7 @@ func (r *Controller) generateBootstrapDataForWorker(ctx context.Context, log log
288391
// https://cluster-api.sigs.k8s.io/developer/providers/contracts/bootstrap-config#sentinel-file
289392
commands = append(commands, "mkdir -p /run/cluster-api && touch /run/cluster-api/bootstrap-success.complete")
290393

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-
})
394+
return commands, commandsMap, nil
309395
}
310396

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

internal/controller/bootstrap/worker_bootstrap_controller_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,39 @@ 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+
"powershell.exe -NoProfile -NonInteractive -Command \"& { k0s.exe install worker --token-file /etc/k0s.token --labels=k0smotron.io/machine-name= --kubelet-extra-args=\"--hostname-override=\" }\"",
148+
"powershell.exe -NoProfile -NonInteractive -Command \"& { k0s.exe start }\"",
149+
},
150+
},
151+
}
152+
153+
for _, tt := range tests {
154+
t.Run(tt.name, func(t *testing.T) {
155+
got, _ := getWindowsCommands(tt.scope)
156+
require.Equal(t, tt.want, got)
157+
})
158+
}
159+
160+
}
161+
129162
func Test_createBootstrapSecret(t *testing.T) {
130163
tests := []struct {
131164
name string

0 commit comments

Comments
 (0)