Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6679186
Extend minikube start command to accept windows os version flag (#1)
bobsira Jun 24, 2024
c2b2daa
Merge branch 'kubernetes:master' into feature/windows-node-support
bobsira Jun 24, 2024
d2afbfa
Merge branch 'kubernetes:master' into feature/windows-node-support
bobsira Jul 15, 2024
bad9f03
Download and cache windows server ISO to preloads folder (#3)
bobsira Aug 6, 2024
9883a43
Merge branch 'kubernetes:master' into feature/windows-node-support
bobsira Aug 6, 2024
54cd55c
Extended minikube node add command to accept flags for adding a windo…
bobsira Aug 7, 2024
08a4ee6
windows node init setup
bobsira Aug 13, 2024
df3e08a
logic for windows node initialization
bobsira Jan 2, 2025
f6ff78c
efforts to reduce node join command time
bobsira Jan 30, 2025
83dc84f
logic for only server 2025
bobsira Feb 11, 2025
3991998
short-circuit for minikube delete for windows
bobsira Feb 17, 2025
293f56c
ssh authnetication refactor
bobsira Feb 20, 2025
4ebe11c
reverted Download and cache windows server ISO to preloads folder PR …
bobsira Feb 20, 2025
26f42a4
modified the tests for NewHost
bobsira Feb 21, 2025
361c0b3
node-os flag implementation
bobsira Apr 4, 2025
4b4c79d
node-os shorthand is more than one ASCII character panic fix
bobsira Apr 5, 2025
b5ef9b0
user personalized server vhd logic
bobsira Jun 16, 2025
2005871
fixed failing tests
bobsira Jun 16, 2025
71e7730
attempts to fix trySigKillProcess test
bobsira Sep 1, 2025
3489fbf
reverting test changes
bobsira Sep 2, 2025
0c7fc2e
Merge feature/windows-node-support into user/bosira/windows-node-init…
bobsira Jan 15, 2026
3d4c149
Ported over machine-minikube Windows node changes
bobsira Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cmd/minikube/cmd/node_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ limitations under the License.
package cmd

import (
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand All @@ -36,13 +39,35 @@ var (
cpNode bool
workerNode bool
deleteNodeOnFailure bool
osType string

osTypeLong = "This flag should only be used when adding a windows node to a cluster.\n\n" +
"Specify the OS of the node to add in the format 'os=OS_TYPE,version=VERSION'.\n" +
"This means that the node to be added will be a Windows node and the version of Windows OS to use for that node is Windows Server 2022.\n" +
"Example: $ minikube node add --os='os=windows,version=2022'\n" +
"Valid options for OS_TYPE are: linux, windows. If not specified, the default value is linux.\n" +
"You do not need to specify the --os flag if you are adding a linux node."
)

var nodeAddCmd = &cobra.Command{
Use: "add",
Short: "Adds a node to the given cluster.",
Long: "Adds a node to the given cluster config, and starts it.",
Run: func(cmd *cobra.Command, _ []string) {

osType, windowsVersion, err := parseOSFlag(osType)
if err != nil {
exit.Message(reason.Usage, "{{.err}}", out.V{"err": err})
}

if err := validateOSandVersion(osType, windowsVersion); err != nil {
exit.Message(reason.Usage, "{{.err}}", out.V{"err": err})
}

if osType == "windows" && cpNode {
exit.Message(reason.Usage, "Windows node cannot be used as control-plane nodes.")
}

co := mustload.Healthy(ClusterFlagValue())
cc := co.Config

Expand Down Expand Up @@ -109,6 +134,42 @@ func init() {
nodeAddCmd.Flags().BoolVar(&cpNode, "control-plane", false, "If set, added node will become a control-plane. Defaults to false. Currently only supported for existing HA (multi-control plane) clusters.")
nodeAddCmd.Flags().BoolVar(&workerNode, "worker", true, "If set, added node will be available as worker. Defaults to true.")
nodeAddCmd.Flags().BoolVar(&deleteNodeOnFailure, "delete-on-failure", false, "If set, delete the current cluster if start fails and try again. Defaults to false.")
nodeAddCmd.Flags().StringVar(&osType, "os", "linux", osTypeLong)

nodeCmd.AddCommand(nodeAddCmd)
}

// parseOSFlag parses the --os flag value , 'os=OS_TYPE,version=VERSION', and returns the os type and version
// For example, 'os=windows,version=2022' The output will be os: 'windows' and version: '2022' respectively
func parseOSFlag(osFlagValue string) (string, string, error) {
// Remove all spaces from the input string
osFlagValue = strings.ReplaceAll(osFlagValue, " ", "")
parts := strings.Split(osFlagValue, ",")
osInfo := map[string]string{
"os": "linux", // default value
"version": "",
}

for _, part := range parts {
kv := strings.Split(part, "=")
if len(kv) != 2 {
return "", "", errors.Errorf("Invalid format for --os flag: %s", osFlagValue)
}
osInfo[kv[0]] = kv[1]
}

// if os is specified to linux, set the version to empty string as it is not required
if osInfo["os"] == "linux" {
if osInfo["version"] != "" {
out.WarningT("Ignoring version flag for linux os. You do not need to specify the version for linux os.")
}
osInfo["version"] = ""
}

// if os is specified to windows and version is not specified, set the default version to 2022(Windows Server 2022)
if osInfo["os"] == "windows" && osInfo["version"] == "" {
osInfo["version"] = "2022"
}

return osInfo["os"], osInfo["version"], nil
}
134 changes: 134 additions & 0 deletions cmd/minikube/cmd/node_add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
Copyright 2024 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"testing"

"github.com/pkg/errors"
)

func TestValidateOS(t *testing.T) {
tests := []struct {
osType string
errorMsg string
}{
{"linux", ""},
{"windows", ""},
{"foo", "Invalid OS: foo. Valid OS are: linux, windows"},
}
for _, test := range tests {
t.Run(test.osType, func(t *testing.T) {
got := validateOS(test.osType)
gotError := ""
if got != nil {
gotError = got.Error()
}
if gotError != test.errorMsg {
t.Errorf("validateOS(osType=%v): got %v, expected %v", test.osType, got, test.errorMsg)
}
})
}
}

// TestParseOSFlag is the main test function for parseOSFlag
func TestParseOSFlag(t *testing.T) {
tests := []struct {
name string
input string
expectedOS string
expectedVer string
expectedErr error
}{
{
name: "Valid input with all fields",
input: "os=windows,version=2019",
expectedOS: "windows",
expectedVer: "2019",
expectedErr: nil,
},
{
name: "Valid input with default version for windows",
input: "os=windows",
expectedOS: "windows",
expectedVer: "2022",
expectedErr: nil,
},
{
name: "Valid input with linux and no version",
input: "os=linux",
expectedOS: "linux",
expectedVer: "",
expectedErr: nil,
},
{
name: "Invalid input with missing version",
input: "os=linux,version=",
expectedOS: "linux",
expectedVer: "",
expectedErr: nil,
},
{
name: "Invalid input with extra comma",
input: "os=linux,version=,",
expectedOS: "",
expectedVer: "",
expectedErr: errors.New("Invalid format for --os flag: os=linux,version=,"),
},
{
name: "Invalid input with no key-value pair",
input: "linux,version=2022",
expectedOS: "",
expectedVer: "",
expectedErr: errors.New("Invalid format for --os flag: linux,version=2022"),
},
{
name: "Valid input with extra spaces",
input: "os=linux , version=latest",
expectedOS: "linux",
expectedVer: "",
expectedErr: nil,
},
{
name: "Valid input with capital letters in keys",
input: "OS=linux,Version=2022",
expectedOS: "linux",
expectedVer: "",
expectedErr: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotOS, gotVer, err := parseOSFlag(tt.input)

if tt.expectedErr != nil && (err == nil || err.Error() != tt.expectedErr.Error()) {
t.Errorf("Expected error %v, got %v", tt.expectedErr, err)
} else if tt.expectedErr == nil && err != nil {
t.Errorf("Expected no error, but got %v", err)
}

if gotOS != tt.expectedOS {
t.Errorf("Expected OS %s, got %s", tt.expectedOS, gotOS)
}

if gotVer != tt.expectedVer {
t.Errorf("Expected version %s, got %s", tt.expectedVer, gotVer)
}
})
}
}
70 changes: 69 additions & 1 deletion cmd/minikube/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
gopshost "github.com/shirou/gopsutil/v3/host"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/exp/maps"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/minikube/pkg/minikube/command"
Expand Down Expand Up @@ -491,15 +492,36 @@ func startWithDriver(cmd *cobra.Command, starter node.Starter, existing *config.
KubernetesVersion: starter.Cfg.KubernetesConfig.KubernetesVersion,
ContainerRuntime: starter.Cfg.KubernetesConfig.ContainerRuntime,
Worker: true,
OS: "linux",
}
if i < numCPNodes { // starter node is also counted as (primary) cp node
n.ControlPlane = true
}
}

out.Ln("") // extra newline for clarity on the command line
// 1st call
if err := node.Add(starter.Cfg, n, viper.GetBool(deleteOnFailure)); err != nil {
return nil, errors.Wrap(err, "adding node")
return nil, errors.Wrap(err, "adding linux node")
}
}

// start windows node. trigger windows node start only if windows node version is set at the time of minikube start
if cmd.Flags().Changed(windowsNodeVersion) {
// TODO: if windows node version is set to windows server 2022 then the windows node name should be minikube-ws2022
nodeName := node.Name(numNodes + 1)
n := config.Node{
Name: nodeName,
Port: starter.Cfg.APIServerPort,
KubernetesVersion: starter.Cfg.KubernetesConfig.KubernetesVersion,
ContainerRuntime: starter.Cfg.KubernetesConfig.ContainerRuntime,
Worker: true,
OS: "windows",
}

out.Ln("") // extra newline for clarity on the command line
if err := node.Add(starter.Cfg, n, viper.GetBool(deleteOnFailure)); err != nil {
return nil, errors.Wrap(err, "adding windows node")
}
}

Expand Down Expand Up @@ -1303,6 +1325,13 @@ func validateFlags(cmd *cobra.Command, drvName string) { //nolint:gocyclo
validateCNI(cmd, viper.GetString(containerRuntime))
}

if cmd.Flags().Changed(windowsNodeVersion) {
if err := validateWindowsOSVersion(viper.GetString(windowsNodeVersion)); err != nil {
exit.Message(reason.Usage, "{{.err}}", out.V{"err": err})
}

}

if cmd.Flags().Changed(staticIP) {
if err := validateStaticIP(viper.GetString(staticIP), drvName, viper.GetString(subnet)); err != nil {
exit.Message(reason.Usage, "{{.err}}", out.V{"err": err})
Expand Down Expand Up @@ -1424,6 +1453,45 @@ func validateDiskSize(diskSize string) error {
return nil
}

// validateWindowsOSVersion validates the supplied window server os version
func validateWindowsOSVersion(osVersion string) error {
validOptions := node.ValidWindowsOSVersions()

if validOptions[osVersion] {
return nil
}

return errors.Errorf("Invalid Windows Server OS Version: %s. Valid OS version are: %s", osVersion, maps.Keys(validOptions))
}

// validateOS validates the supplied OS
func validateOS(os string) error {
validOptions := node.ValidOS()

for _, validOS := range validOptions {
if os == validOS {
return nil
}
}

return errors.Errorf("Invalid OS: %s. Valid OS are: %s", os, strings.Join(validOptions, ", "))
}

// validateOSandVersion validates the supplied OS and version
func validateOSandVersion(os, version string) error {

if err := validateOS(os); err != nil {
return err
}

if version != "" {
if err := validateWindowsOSVersion(version); err != nil {
return err
}
}
return nil
}

// validateRuntime validates the supplied runtime
func validateRuntime(rtime string) error {
validOptions := cruntime.ValidRuntimes()
Expand Down
5 changes: 5 additions & 0 deletions cmd/minikube/cmd/start_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const (
staticIP = "static-ip"
gpus = "gpus"
autoPauseInterval = "auto-pause-interval"
windowsNodeVersion = "windows-node-version"
)

var (
Expand Down Expand Up @@ -208,6 +209,8 @@ func initMinikubeFlags() {
startCmd.Flags().String(staticIP, "", "Set a static IP for the minikube cluster, the IP must be: private, IPv4, and the last octet must be between 2 and 254, for example 192.168.200.200 (Docker and Podman drivers only)")
startCmd.Flags().StringP(gpus, "g", "", "Allow pods to use your NVIDIA GPUs. Options include: [all,nvidia] (Docker driver with Docker container-runtime only)")
startCmd.Flags().Duration(autoPauseInterval, time.Minute*1, "Duration of inactivity before the minikube VM is paused (default 1m0s)")
startCmd.Flags().String(windowsNodeVersion, constants.DefaultWindowsNodeVersion, "The version of Windows to use for the Windows node on a multi-node cluster (e.g., 2019, 2022). Defaults to Windows Server 2022")

}

// initKubernetesFlags inits the commandline flags for Kubernetes related options
Expand Down Expand Up @@ -586,6 +589,7 @@ func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, rtime str
SocketVMnetClientPath: detect.SocketVMNetClientPath(),
SocketVMnetPath: detect.SocketVMNetPath(),
StaticIP: viper.GetString(staticIP),
WindowsNodeVersion: viper.GetString(windowsNodeVersion),
KubernetesConfig: config.KubernetesConfig{
KubernetesVersion: k8sVersion,
ClusterName: ClusterFlagValue(),
Expand Down Expand Up @@ -837,6 +841,7 @@ func updateExistingConfigFromFlags(cmd *cobra.Command, existing *config.ClusterC
updateStringFromFlag(cmd, &cc.SocketVMnetClientPath, socketVMnetClientPath)
updateStringFromFlag(cmd, &cc.SocketVMnetPath, socketVMnetPath)
updateDurationFromFlag(cmd, &cc.AutoPauseInterval, autoPauseInterval)
updateStringFromFlag(cmd, &cc.WindowsNodeVersion, windowsNodeVersion)

if cmd.Flags().Changed(kubernetesVersion) {
kubeVer, err := getKubernetesVersion(existing)
Expand Down
28 changes: 28 additions & 0 deletions cmd/minikube/cmd/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,34 @@ func TestValidateRuntime(t *testing.T) {
}
}

func TestValidateWindowsOSVersion(t *testing.T) {
var tests = []struct {
osVersion string
errorMsg string
}{
{
osVersion: "2025",
errorMsg: "",
},
{
osVersion: "2023",
errorMsg: "Invalid Windows Server OS Version: 2023. Valid OS version are: [2025]",
},
}
for _, test := range tests {
t.Run(test.osVersion, func(t *testing.T) {
got := validateWindowsOSVersion(test.osVersion)
gotError := ""
if got != nil {
gotError = got.Error()
}
if gotError != test.errorMsg {
t.Errorf("ValidateWindowsOSVersion(osVersion=%v): got %v, expected %v", test.osVersion, got, test.errorMsg)
}
})
}
}

func TestIsTwoDigitSemver(t *testing.T) {
var tcs = []struct {
desc string
Expand Down
Loading
Loading