Merge 6ccfb43fdf into d5796e6cc5
commit
880f6e3b30
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
|
|
@ -37,6 +40,14 @@ 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{
|
||||
|
|
@ -44,6 +55,20 @@ var nodeAddCmd = &cobra.Command{
|
|||
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.")
|
||||
}
|
||||
|
||||
options := flags.CommandOptions()
|
||||
|
||||
co := mustload.Healthy(ClusterFlagValue(), options)
|
||||
|
|
@ -112,6 +137,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ import (
|
|||
gopshost "github.com/shirou/gopsutil/v4/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/klog/v2"
|
||||
|
|
@ -193,6 +194,14 @@ func runStart(cmd *cobra.Command, _ []string) {
|
|||
out.WarningT("Profile name '{{.name}}' is not valid", out.V{"name": ClusterFlagValue()})
|
||||
exit.Message(reason.Usage, "Only alphanumeric and dashes '-' are permitted. Minimum 2 characters, starting with alphanumeric.")
|
||||
}
|
||||
|
||||
// change the driver to hyperv, cni to flannel and container runtime to containerd if we have --node-os=windows
|
||||
if cmd.Flags().Changed(nodeOS) {
|
||||
viper.Set("driver", driver.HyperV)
|
||||
viper.Set("cni", "flannel")
|
||||
viper.Set(containerRuntime, constants.Containerd)
|
||||
|
||||
}
|
||||
existing, err := config.Load(ClusterFlagValue())
|
||||
if err != nil && !config.IsNotExist(err) {
|
||||
kind := reason.HostConfigLoad
|
||||
|
|
@ -495,6 +504,13 @@ func startWithDriver(cmd *cobra.Command, starter node.Starter, existing *config.
|
|||
// target total and number of control-plane nodes
|
||||
numCPNodes := 1
|
||||
numNodes := viper.GetInt(nodes)
|
||||
// if we have -node-os flag set, then nodes flag will be set to 2
|
||||
// it means one of the nodes is a control-plane node and the other is a windows worker node
|
||||
// so we need to reduce the numNodes by 1
|
||||
if cmd.Flags().Changed(nodeOS) {
|
||||
numNodes--
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
numCPNodes = 0
|
||||
for _, n := range existing.Nodes {
|
||||
|
|
@ -520,6 +536,11 @@ func startWithDriver(cmd *cobra.Command, starter node.Starter, existing *config.
|
|||
KubernetesVersion: starter.Cfg.KubernetesConfig.KubernetesVersion,
|
||||
ContainerRuntime: starter.Cfg.KubernetesConfig.ContainerRuntime,
|
||||
Worker: true,
|
||||
Guest: config.Guest{
|
||||
Name: "linux",
|
||||
Version: "latest",
|
||||
URL: "",
|
||||
},
|
||||
}
|
||||
if i < numCPNodes { // starter node is also counted as (primary) cp node
|
||||
n.ControlPlane = true
|
||||
|
|
@ -527,8 +548,34 @@ func startWithDriver(cmd *cobra.Command, starter node.Starter, existing *config.
|
|||
}
|
||||
|
||||
out.Ln("") // extra newline for clarity on the command line
|
||||
// 1st call
|
||||
if err := node.Add(starter.Cfg, n, viper.GetBool(deleteOnFailure), options); err != nil {
|
||||
return nil, fmt.Errorf("adding node: %w", err)
|
||||
return nil, fmt.Errorf("adding linux node: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// we currently trigger the windows node start if the user has set the --windows-node-version or --node-os flag
|
||||
// we might need to get rid of --windows-node-version in the future and just use --node-os flag
|
||||
// start windows node. trigger windows node start if windows node version or node node os is set at the time of minikube start
|
||||
if cmd.Flags().Changed(windowsNodeVersion) || cmd.Flags().Changed(nodeOS) {
|
||||
// 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,
|
||||
Guest: config.Guest{
|
||||
Name: "windows",
|
||||
Version: viper.GetString(windowsNodeVersion),
|
||||
URL: viper.GetString(windowsVhdURL),
|
||||
},
|
||||
}
|
||||
|
||||
out.Ln("") // extra newline for clarity on the command line
|
||||
if err := node.Add(starter.Cfg, n, viper.GetBool(deleteOnFailure), options); err != nil {
|
||||
return nil, fmt.Errorf("adding windows node: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1339,6 +1386,37 @@ 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(nodeOS) {
|
||||
if err := validMultiNodeOS(viper.GetString(nodeOS)); err != nil {
|
||||
exit.Message(reason.Usage, "{{.err}}", out.V{"err": err})
|
||||
}
|
||||
|
||||
if viper.GetInt(nodes) != 2 {
|
||||
exit.Message(reason.Usage, "The --nodes flag must be set to 2 when using --node-os")
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed(windowsVhdURL) {
|
||||
if viper.GetString(windowsVhdURL) == "" {
|
||||
// set a default URL if the user has not specified one
|
||||
viper.Set(windowsVhdURL, constants.DefaultWindowsVhdURL)
|
||||
exit.Message(reason.Usage, "The --windows-vhd-url flag must be set to a valid URL")
|
||||
}
|
||||
|
||||
// add validation logic for the windows vhd URL
|
||||
url := viper.GetString(windowsVhdURL)
|
||||
if !strings.HasSuffix(url, ".vhd") && !strings.HasSuffix(url, ".vhdx") {
|
||||
exit.Message(reason.Usage, "The --windows-vhd-url flag must point to a valid VHD or VHDX file")
|
||||
}
|
||||
} ////
|
||||
|
||||
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})
|
||||
|
|
@ -1457,6 +1535,63 @@ 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 fmt.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 fmt.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
|
||||
}
|
||||
|
||||
// validateMultiNodeOS validates the supplied OS for multiple nodes
|
||||
func validMultiNodeOS(osString string) error {
|
||||
if !strings.HasPrefix(osString, "[") || !strings.HasSuffix(osString, "]") {
|
||||
return fmt.Errorf("invalid OS string format: must be enclosed in [ ]")
|
||||
}
|
||||
|
||||
osString = strings.Trim(osString, "[]")
|
||||
osString = strings.ReplaceAll(osString, " ", "")
|
||||
|
||||
osValues := strings.Split(osString, ",")
|
||||
|
||||
if len(osValues) != 2 || osValues[0] != "linux" || osValues[1] != "windows" {
|
||||
return fmt.Errorf("invalid OS string format: must be [linux,windows]")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRuntime validates the supplied runtime
|
||||
func validateRuntime(rtime string) error {
|
||||
validOptions := cruntime.ValidRuntimes()
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ const (
|
|||
ha = "ha"
|
||||
nodes = "nodes"
|
||||
preload = "preload"
|
||||
preloadWindowsIso = "preload-windows-iso"
|
||||
deleteOnFailure = "delete-on-failure"
|
||||
forceSystemd = "force-systemd"
|
||||
kicBaseImage = "base-image"
|
||||
|
|
@ -145,6 +146,9 @@ const (
|
|||
staticIP = "static-ip"
|
||||
gpus = "gpus"
|
||||
autoPauseInterval = "auto-pause-interval"
|
||||
windowsNodeVersion = "windows-node-version"
|
||||
nodeOS = "node-os"
|
||||
windowsVhdURL = "windows-vhd-url"
|
||||
preloadSrc = "preload-source"
|
||||
rosetta = "rosetta"
|
||||
)
|
||||
|
|
@ -190,13 +194,14 @@ func initMinikubeFlags() {
|
|||
startCmd.Flags().Bool(enableDefaultCNI, false, "DEPRECATED: Replaced by --cni=bridge")
|
||||
startCmd.Flags().String(cniFlag, "", "CNI plug-in to use. Valid options: auto, bridge, calico, cilium, flannel, kindnet, or path to a CNI manifest (default: auto)")
|
||||
startCmd.Flags().StringSlice(waitComponents, kverify.DefaultWaitList, fmt.Sprintf("comma separated list of Kubernetes components to verify and wait for after starting a cluster. defaults to %q, available options: %q . other acceptable values are 'all' or 'none', 'true' and 'false'", strings.Join(kverify.DefaultWaitList, ","), strings.Join(kverify.AllComponentsList, ",")))
|
||||
startCmd.Flags().Duration(waitTimeout, 6*time.Minute, "max time to wait per Kubernetes or host to be healthy.")
|
||||
startCmd.Flags().Duration(waitTimeout, 12*time.Minute, "max time to wait per Kubernetes or host to be healthy.")
|
||||
startCmd.Flags().Bool(nativeSSH, true, "Use native Golang SSH client (default true). Set to 'false' to use the command line 'ssh' command when accessing the docker machine. Useful for the machine drivers when they will not start with 'Waiting for SSH'.")
|
||||
startCmd.Flags().Bool(autoUpdate, true, "If set, automatically updates drivers to the latest version. Defaults to true.")
|
||||
startCmd.Flags().Bool(installAddons, true, "If set, install addons. Defaults to true.")
|
||||
startCmd.Flags().Bool(ha, false, "Create Highly Available Multi-Control Plane Cluster with a minimum of three control-plane nodes that will also be marked for work.")
|
||||
startCmd.Flags().IntP(nodes, "n", 1, "The total number of nodes to spin up. Defaults to 1.")
|
||||
startCmd.Flags().Bool(preload, true, "If set, download tarball of preloaded images if available to improve start time. Defaults to true.")
|
||||
startCmd.Flags().Bool(preloadWindowsIso, false, "If set, download the Windows ISO to improve start time of setting up a windows node. Defaults to false.")
|
||||
startCmd.Flags().Bool(noKubernetes, false, "If set, minikube VM/container will start without starting or configuring Kubernetes. (only works on new clusters)")
|
||||
startCmd.Flags().Bool(deleteOnFailure, false, "If set, delete the current cluster if start fails and try again. Defaults to false.")
|
||||
startCmd.Flags().Bool(forceSystemd, false, "If set, force the container runtime to use systemd as cgroup manager. Defaults to false.")
|
||||
|
|
@ -212,7 +217,12 @@ 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 GPUs. Options include: [all,nvidia,amd] (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., 2025). Currently support Windows Server 2025")
|
||||
startCmd.Flags().String(nodeOS, "node-os", "The OS to use for the node. Currently support 'linux, windows'. If not set, it will be set to the same as the control plane node.")
|
||||
startCmd.Flags().String(windowsVhdURL, constants.DefaultWindowsVhdURL, "The VHD URL to use for the windows node on a multi-node cluster. If not set, it will be set to the default Windows Server 2025 VHD URL.")
|
||||
|
||||
startCmd.Flags().String(preloadSrc, "auto", "Which source to download the preload from (valid options: gcs, github, auto). Defaults to auto (try both).")
|
||||
|
||||
}
|
||||
|
||||
// initKubernetesFlags inits the commandline flags for Kubernetes related options
|
||||
|
|
@ -655,6 +665,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(),
|
||||
|
|
@ -907,6 +918,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)
|
||||
updateBoolFromFlag(cmd, &cc.Rosetta, rosetta)
|
||||
|
||||
if cmd.Flags().Changed(kubernetesVersion) {
|
||||
|
|
|
|||
|
|
@ -477,6 +477,79 @@ 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 TestValidMultiNodeOS(t *testing.T) {
|
||||
var tests = []struct {
|
||||
osString string
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
osString: "[linux,windows]",
|
||||
errorMsg: "",
|
||||
},
|
||||
{
|
||||
osString: "[linux, windows]",
|
||||
errorMsg: "",
|
||||
},
|
||||
{
|
||||
osString: "[windows,linux]",
|
||||
errorMsg: "invalid OS string format: must be [linux,windows]",
|
||||
},
|
||||
{
|
||||
osString: "[linux]",
|
||||
errorMsg: "invalid OS string format: must be [linux,windows]",
|
||||
},
|
||||
{
|
||||
osString: "[linux,windows,mac]",
|
||||
errorMsg: "invalid OS string format: must be [linux,windows]",
|
||||
},
|
||||
{
|
||||
osString: "linux,windows",
|
||||
errorMsg: "invalid OS string format: must be enclosed in [ ]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.osString, func(t *testing.T) {
|
||||
got := validMultiNodeOS(test.osString)
|
||||
gotError := ""
|
||||
if got != nil {
|
||||
gotError = got.Error()
|
||||
}
|
||||
if gotError != test.errorMsg {
|
||||
t.Errorf("validMultiNodeOS(osString=%v): got %v, expected %v", test.osString, gotError, test.errorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTwoDigitSemver(t *testing.T) {
|
||||
var tcs = []struct {
|
||||
desc string
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -47,6 +47,7 @@ require (
|
|||
github.com/otiai10/copy v1.14.1
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
|
|
@ -197,7 +198,6 @@ require (
|
|||
github.com/otiai10/mint v1.6.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/xattr v0.4.9 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ require (
|
|||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/image v0.25.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
|
|
|
|||
|
|
@ -18,15 +18,17 @@ package hyperv
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
"k8s.io/minikube/pkg/libmachine/drivers"
|
||||
"k8s.io/minikube/pkg/libmachine/log"
|
||||
"k8s.io/minikube/pkg/libmachine/mcnflag"
|
||||
|
|
@ -38,6 +40,7 @@ import (
|
|||
type Driver struct {
|
||||
*drivers.BaseDriver
|
||||
Boot2DockerURL string
|
||||
WindowsVHDUrl string
|
||||
VSwitch string
|
||||
DiskSize int
|
||||
MemSize int
|
||||
|
|
@ -45,6 +48,7 @@ type Driver struct {
|
|||
MacAddr string
|
||||
VLanID int
|
||||
DisableDynamicMemory bool
|
||||
OS string
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -54,6 +58,7 @@ const (
|
|||
defaultVLanID = 0
|
||||
defaultDisableDynamicMemory = false
|
||||
defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444"
|
||||
defaultServerImageFilename = "hybrid-minikube-windows-server.vhdx"
|
||||
)
|
||||
|
||||
// NewDriver creates a new Hyper-v driver with default settings.
|
||||
|
|
@ -62,6 +67,7 @@ func NewDriver(hostName, storePath string) *Driver {
|
|||
DiskSize: defaultDiskSize,
|
||||
MemSize: defaultMemory,
|
||||
CPU: defaultCPU,
|
||||
WindowsVHDUrl: mcnutils.ConfigGuest.GetVHDUrl(),
|
||||
DisableDynamicMemory: defaultDisableDynamicMemory,
|
||||
BaseDriver: &drivers.BaseDriver{
|
||||
MachineName: hostName,
|
||||
|
|
@ -205,17 +211,82 @@ func (d *Driver) PreCreateCheck() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Downloading boot2docker to cache should be done here to make sure
|
||||
// Downloading boot2docker/windows-server to cache should be done here to make sure
|
||||
// that a download failure will not leave a machine half created.
|
||||
b2dutils := mcnutils.NewB2dUtils(d.StorePath)
|
||||
err = b2dutils.UpdateISOCache(d.Boot2DockerURL)
|
||||
if mcnutils.ConfigGuest.GetGuestOS() != "windows" {
|
||||
err = b2dutils.UpdateISOCache(d.Boot2DockerURL)
|
||||
} else {
|
||||
err = b2dutils.UpdateVHDCache(d.WindowsVHDUrl)
|
||||
}
|
||||
|
||||
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
|
||||
vhdxPath := filepath.Join(b2dutils.GetImgCachePath(), defaultServerImageFilename)
|
||||
|
||||
mounted := false
|
||||
defer func() {
|
||||
if mounted {
|
||||
if err := cmd("Hyper-V\\Dismount-VHD", "-Path", quote(vhdxPath)); err != nil {
|
||||
log.Errorf("failed to dismount VHDX %s: %v", vhdxPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd("Hyper-V\\Mount-VHD", "-Path", quote(vhdxPath), "-ReadOnly"); err != nil {
|
||||
return fmt.Errorf("failed to mount VHDX: %w", err)
|
||||
}
|
||||
mounted = true
|
||||
|
||||
diskNumOut, err := cmdOut("-Command", fmt.Sprintf(
|
||||
"$d=(Get-VHD -Path %s | Get-Disk).Number; if($d -ne $null){$d}else{''}",
|
||||
quote(vhdxPath),
|
||||
))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine disk number for VHDX: %w", err)
|
||||
}
|
||||
|
||||
diskNumber := strings.TrimSpace(diskNumOut)
|
||||
if diskNumber == "" {
|
||||
return fmt.Errorf("could not determine disk number for mounted VHDX")
|
||||
}
|
||||
|
||||
avail, err := cmdOut("-Command",
|
||||
"$used=(Get-PSDrive -PSProvider FileSystem).Name; "+
|
||||
"$letters='DEFGHIJKLMNOPQRSTUVWXYZ'.ToCharArray(); "+
|
||||
"$free=$letters | Where-Object { $used -notcontains $_ }; "+
|
||||
"if ($free) { $free[0] } else { '' }",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine available drive letter: %w", err)
|
||||
}
|
||||
|
||||
freeLetter := strings.TrimSpace(avail)
|
||||
if freeLetter == "" {
|
||||
return fmt.Errorf("no available drive letters to assign to VHDX")
|
||||
}
|
||||
|
||||
if err := cmd("Set-Partition",
|
||||
"-DiskNumber", diskNumber,
|
||||
"-PartitionNumber", "4",
|
||||
"-NewDriveLetter", freeLetter,
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to set partition on VHDX: %w", err)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Driver) Create() error {
|
||||
b2dutils := mcnutils.NewB2dUtils(d.StorePath)
|
||||
if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil {
|
||||
return err
|
||||
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
|
||||
d.SSHUser = "Administrator"
|
||||
if err := b2dutils.CopyWindowsVHDToMachineDir(d.WindowsVHDUrl, d.MachineName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Creating SSH key...")
|
||||
|
|
@ -233,15 +304,33 @@ func (d *Driver) Create() error {
|
|||
}
|
||||
log.Infof("Using switch %q", d.VSwitch)
|
||||
|
||||
diskImage, err := d.generateDiskImage()
|
||||
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
|
||||
log.Infof("Adding SSH key to the VHDX...")
|
||||
if err := writeSSHKeyToVHDX(d.ResolveStorePath(defaultServerImageFilename), d.publicSSHKeyPath()); err != nil {
|
||||
log.Errorf("Error creating disk image: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var diskImage string
|
||||
var err error
|
||||
if mcnutils.ConfigGuest.GetGuestOS() != "windows" {
|
||||
diskImage, err = d.generateDiskImage()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vmGeneration := "1"
|
||||
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
|
||||
vmGeneration = "2"
|
||||
}
|
||||
|
||||
if err := cmd("Hyper-V\\New-VM",
|
||||
d.MachineName,
|
||||
"-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")),
|
||||
"-SwitchName", quote(d.VSwitch),
|
||||
"-Generation", quote(vmGeneration),
|
||||
"-MemoryStartupBytes", toMb(d.MemSize)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -256,7 +345,8 @@ func (d *Driver) Create() error {
|
|||
if d.CPU > 1 {
|
||||
if err := cmd("Hyper-V\\Set-VMProcessor",
|
||||
d.MachineName,
|
||||
"-Count", fmt.Sprintf("%d", d.CPU)); err != nil {
|
||||
"-Count", fmt.Sprintf("%d", d.CPU),
|
||||
"-ExposeVirtualizationExtensions", "$true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -278,16 +368,28 @@ func (d *Driver) Create() error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := cmd("Hyper-V\\Set-VMDvdDrive",
|
||||
"-VMName", d.MachineName,
|
||||
"-Path", quote(d.ResolveStorePath("boot2docker.iso"))); err != nil {
|
||||
return err
|
||||
if mcnutils.ConfigGuest.GetGuestOS() != "windows" {
|
||||
log.Infof("Attaching ISO and disk...")
|
||||
if err := cmd("Hyper-V\\Set-VMDvdDrive",
|
||||
"-VMName", d.MachineName,
|
||||
"-Path", quote(d.ResolveStorePath("boot2docker.iso"))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd("Hyper-V\\Add-VMHardDiskDrive",
|
||||
"-VMName", d.MachineName,
|
||||
"-Path", quote(diskImage)); err != nil {
|
||||
return err
|
||||
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
|
||||
if err := cmd("Hyper-V\\Add-VMHardDiskDrive",
|
||||
"-VMName", d.MachineName,
|
||||
"-Path", quote(d.ResolveStorePath("hybrid-minikube-windows-server.vhdx")),
|
||||
"-ControllerType", "SCSI"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := cmd("Hyper-V\\Add-VMHardDiskDrive",
|
||||
"-VMName", d.MachineName,
|
||||
"-Path", quote(diskImage)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Starting VM...")
|
||||
|
|
@ -529,3 +631,58 @@ func (d *Driver) generateDiskImage() (string, error) {
|
|||
|
||||
return diskImage, nil
|
||||
}
|
||||
|
||||
func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) (retErr error) {
|
||||
output, err := cmdOut(
|
||||
"-Command",
|
||||
"(Get-DiskImage -ImagePath", quote(vhdxPath), "| Mount-DiskImage -PassThru) | Out-Null;",
|
||||
"$diskNumber = (Get-DiskImage -ImagePath", quote(vhdxPath), "| Get-Disk).Number;",
|
||||
"Set-Disk -Number $diskNumber -IsReadOnly $false;",
|
||||
"(Get-Disk -Number $diskNumber | Get-Partition | Get-Volume).DriveLetter",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mount VHDX and retrieve mount directory: %w", err)
|
||||
}
|
||||
|
||||
regex := regexp.MustCompile(`\s+|\r|\n`)
|
||||
driveLetter := regex.ReplaceAllString(output, "")
|
||||
|
||||
if driveLetter == "" {
|
||||
log.Debugf("No drive letter assigned to VHDX")
|
||||
return errors.New("no drive letter assigned to VHDX")
|
||||
}
|
||||
|
||||
mountDir := strings.TrimSpace(driveLetter) + ":" + string(os.PathSeparator)
|
||||
|
||||
defer func() {
|
||||
if unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath)); unmountErr != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("failed to unmount VHDX: %w", unmountErr))
|
||||
}
|
||||
}()
|
||||
|
||||
sshDir := filepath.Join(mountDir, "ProgramData", "ssh")
|
||||
adminAuthKeys := filepath.Join(sshDir, "administrators_authorized_keys")
|
||||
|
||||
pubKey, err := os.ReadFile(publicSSHKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read public SSH key from %s: %w", publicSSHKeyPath, err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mountDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("mount point %s does not exist", mountDir)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(sshDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create SSH directory: %w", err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(adminAuthKeys, pubKey, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write public key: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd("icacls.exe", quote(adminAuthKeys), "/inheritance:r", "/grant", "Administrators:F", "/grant", "SYSTEM:F"); err != nil {
|
||||
return fmt.Errorf("failed to set permissions on %s: %w", adminAuthKeys, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ type Host struct {
|
|||
HostOptions *Options
|
||||
Name string
|
||||
RawDriver []byte `json:"-"`
|
||||
Guest Guest
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
|
|
@ -71,6 +72,12 @@ type Options struct {
|
|||
AuthOptions *auth.Options
|
||||
}
|
||||
|
||||
type Guest struct {
|
||||
Name string
|
||||
Version string
|
||||
URL string
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
ConfigVersion int
|
||||
DriverName string
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ import (
|
|||
|
||||
type API interface {
|
||||
io.Closer
|
||||
NewHost(driverName string, rawDriver []byte) (*host.Host, error)
|
||||
NewHost(driverName string, guest host.Guest, rawDriver []byte) (*host.Host, error)
|
||||
DefineGuest(h *host.Host)
|
||||
Create(h *host.Host) error
|
||||
persist.Store
|
||||
GetMachinesDir() string
|
||||
|
|
@ -85,7 +86,7 @@ func NewClient(storePath, certsDir string) *Client {
|
|||
}
|
||||
}
|
||||
|
||||
func (api *Client) NewHost(driverName string, rawDriver []byte) (*host.Host, error) {
|
||||
func (api *Client) NewHost(driverName string, guest host.Guest, rawDriver []byte) (*host.Host, error) {
|
||||
driver, err := api.clientDriverFactory.NewRPCClientDriver(driverName, rawDriver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -96,6 +97,7 @@ func (api *Client) NewHost(driverName string, rawDriver []byte) (*host.Host, err
|
|||
Name: driver.GetMachineName(),
|
||||
Driver: driver,
|
||||
DriverName: driver.DriverName(),
|
||||
Guest: guest,
|
||||
HostOptions: &host.Options{
|
||||
AuthOptions: &auth.Options{
|
||||
CertDir: api.certsDir,
|
||||
|
|
@ -145,6 +147,10 @@ func (api *Client) Load(name string) (*host.Host, error) {
|
|||
return h, nil
|
||||
}
|
||||
|
||||
func (api *Client) DefineGuest(h *host.Host) {
|
||||
mcnutils.SetGuestUtil(h.Guest.Name, h.Guest.URL)
|
||||
}
|
||||
|
||||
// Create is the wrapper method which covers all of the boilerplate around
|
||||
// actually creating, provisioning, and persisting an instance in the store.
|
||||
func (api *Client) Create(h *host.Host) error {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func (api *FakeAPI) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (api *FakeAPI) NewHost(driverName string, rawDriver []byte) (*host.Host, error) {
|
||||
func (api *FakeAPI) NewHost(driverName string, guestOS string, rawDriver []byte) (*host.Host, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,12 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases"
|
||||
defaultISOFilename = "boot2docker.iso"
|
||||
defaultVolumeIDOffset = int64(0x8028)
|
||||
versionPrefix = "-v"
|
||||
defaultVolumeIDLength = 32
|
||||
defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases"
|
||||
defaultISOFilename = "boot2docker.iso"
|
||||
defaultServerImageFilename = "hybrid-minikube-windows-server.vhdx"
|
||||
defaultVolumeIDOffset = int64(0x8028)
|
||||
versionPrefix = "-v"
|
||||
defaultVolumeIDLength = 32
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -279,6 +280,10 @@ type iso interface {
|
|||
path() string
|
||||
// exists reports whether the ISO exists.
|
||||
exists() bool
|
||||
// pathVHD returns the path of the VHD.
|
||||
pathVHD() string
|
||||
// hasVHD returns whether the server VHD exists.
|
||||
hasVHD() bool
|
||||
// version returns version information of the ISO.
|
||||
version() (string, error)
|
||||
}
|
||||
|
|
@ -287,6 +292,8 @@ type iso interface {
|
|||
type b2dISO struct {
|
||||
// path of Boot2Docker ISO
|
||||
commonIsoPath string
|
||||
// path of Windows Server VHD
|
||||
commonVHDPath string
|
||||
|
||||
// offset and length of ISO volume ID
|
||||
// cf. http://serverfault.com/questions/361474/is-there-a-way-to-change-a-iso-files-volume-id-from-the-command-line
|
||||
|
|
@ -310,6 +317,22 @@ func (b *b2dISO) exists() bool {
|
|||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func (b *b2dISO) pathVHD() string {
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
return b.commonVHDPath
|
||||
}
|
||||
|
||||
func (b *b2dISO) hasVHD() bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := os.Stat(b.commonVHDPath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// version scans the volume ID in b and returns its version tag.
|
||||
func (b *b2dISO) version() (string, error) {
|
||||
if b == nil {
|
||||
|
|
@ -366,6 +389,7 @@ func NewB2dUtils(storePath string) *B2dUtils {
|
|||
releaseGetter: &b2dReleaseGetter{isoFilename: defaultISOFilename},
|
||||
iso: &b2dISO{
|
||||
commonIsoPath: filepath.Join(imgCachePath, defaultISOFilename),
|
||||
commonVHDPath: filepath.Join(imgCachePath, defaultServerImageFilename),
|
||||
volumeIDOffset: defaultVolumeIDOffset,
|
||||
volumeIDLength: defaultVolumeIDLength,
|
||||
},
|
||||
|
|
@ -374,12 +398,22 @@ func NewB2dUtils(storePath string) *B2dUtils {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *B2dUtils) GetImgCachePath() string {
|
||||
return b.imgCachePath
|
||||
}
|
||||
|
||||
// DownloadISO downloads boot2docker ISO image for the given tag and save it at dest.
|
||||
func (b *B2dUtils) DownloadISO(dir, file, isoURL string) error {
|
||||
log.Infof("Downloading %s from %s...", b.path(), isoURL)
|
||||
return b.download(dir, file, isoURL)
|
||||
}
|
||||
|
||||
// DownloadVHD downloads the Windows Server VHD image and saves it at dest.
|
||||
func (b *B2dUtils) DownloadVHD(dir, file, vhdURL string) error {
|
||||
log.Infof("Downloading %s from %s...", b.pathVHD(), vhdURL)
|
||||
return b.download(dir, file, vhdURL)
|
||||
}
|
||||
|
||||
type ReaderWithProgress struct {
|
||||
io.ReadCloser
|
||||
out io.Writer
|
||||
|
|
@ -429,6 +463,31 @@ func (b *B2dUtils) DownloadISOFromURL(latestReleaseURL string) error {
|
|||
return b.DownloadISO(b.imgCachePath, b.filename(), latestReleaseURL)
|
||||
}
|
||||
|
||||
func (b *B2dUtils) UpdateVHDCache(defaultVHDUrl string) error {
|
||||
// recreate the cache dir if it has been manually deleted
|
||||
// this will already be taken care of by the UpdateISOCache method for linux ISO
|
||||
|
||||
exists := b.hasVHD()
|
||||
|
||||
if !exists {
|
||||
log.Info("No default Windows Server VHD found locally, downloading the latest release...")
|
||||
|
||||
filePath := filepath.Join(b.imgCachePath, defaultServerImageFilename)
|
||||
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf(" * Downloading and caching Windows Server VHD image...\n")
|
||||
fmt.Printf(" * This may take a while...\n")
|
||||
err := DownloadVHDX(defaultVHDUrl, filePath, 16, 1) // Download using 16 parts
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %v", err)
|
||||
}
|
||||
log.Info("Windows Server VHD downloaded successfully")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *B2dUtils) UpdateISOCache(isoURL string) error {
|
||||
// recreate the cache dir if it has been manually deleted
|
||||
if _, err := os.Stat(b.imgCachePath); os.IsNotExist(err) {
|
||||
|
|
@ -487,6 +546,24 @@ func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error {
|
|||
return b.DownloadISO(machineDir, b.filename(), downloadURL)
|
||||
}
|
||||
|
||||
func (b *B2dUtils) CopyWindowsVHDToMachineDir(VHDUrl, machineName string) error {
|
||||
|
||||
if err := b.UpdateVHDCache(VHDUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
machineDir := filepath.Join(b.storePath, "machines", machineName)
|
||||
|
||||
windowsMachineVHDPath := filepath.Join(machineDir, defaultServerImageFilename)
|
||||
|
||||
// cached location of the windows iso
|
||||
windowsVHDPath := filepath.Join(b.imgCachePath, defaultServerImageFilename)
|
||||
|
||||
log.Infof("Copying %s to %s...", windowsVHDPath, windowsMachineVHDPath)
|
||||
return CopyFile(windowsVHDPath, windowsMachineVHDPath)
|
||||
|
||||
}
|
||||
|
||||
// isLatest checks the latest release tag and
|
||||
// reports whether the local ISO cache is the latest version.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -216,10 +216,12 @@ func (m *mockReleaseGetter) download(dir, file, isoURL string) error {
|
|||
}
|
||||
|
||||
type mockISO struct {
|
||||
isopath string
|
||||
exist bool
|
||||
ver string
|
||||
verCh <-chan string
|
||||
isopath string
|
||||
exist bool
|
||||
ver string
|
||||
vhdpath string
|
||||
vhdexist bool
|
||||
verCh <-chan string
|
||||
}
|
||||
|
||||
func (m *mockISO) path() string {
|
||||
|
|
@ -230,6 +232,14 @@ func (m *mockISO) exists() bool {
|
|||
return m.exist
|
||||
}
|
||||
|
||||
func (m *mockISO) pathVHD() string {
|
||||
return m.vhdpath
|
||||
}
|
||||
|
||||
func (m *mockISO) hasVHD() bool {
|
||||
return m.vhdexist
|
||||
}
|
||||
|
||||
func (m *mockISO) version() (string, error) {
|
||||
select {
|
||||
// receive version of a downloaded iso
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
Copyright 2026 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 mcnutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/minikube/pkg/libmachine/log"
|
||||
)
|
||||
|
||||
type ProgressWriter struct {
|
||||
Total int64
|
||||
Downloaded int64
|
||||
mu sync.Mutex
|
||||
TargetName string
|
||||
}
|
||||
|
||||
func NewProgressWriter(total int64, targetName string) *ProgressWriter {
|
||||
return &ProgressWriter{Total: total, TargetName: targetName}
|
||||
}
|
||||
|
||||
func (pw *ProgressWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
pw.mu.Lock()
|
||||
pw.Downloaded += int64(n)
|
||||
pw.printProgress()
|
||||
pw.mu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (pw *ProgressWriter) printProgress() {
|
||||
// Overwrite the same line with \r
|
||||
fmt.Printf("\r > %s: %d / %d bytes complete", pw.TargetName, pw.Downloaded, pw.Total)
|
||||
}
|
||||
|
||||
// copyLocalFile copies from a local source path to destination, reporting progress.
|
||||
func copyLocalFile(srcPath, dstPath string) error {
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open local source file %q: %w", srcPath, err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Get total size
|
||||
info, err := srcFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat local source file %q: %w", srcPath, err)
|
||||
}
|
||||
totalSize := info.Size()
|
||||
|
||||
outFile, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file %q: %w", dstPath, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
pw := NewProgressWriter(totalSize, filepath.Base(dstPath))
|
||||
// Use TeeReader: read from srcFile, write to pw (for progress), and to outFile
|
||||
_, err = io.Copy(outFile, io.TeeReader(srcFile, pw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying local file to %q: %w", dstPath, err)
|
||||
}
|
||||
|
||||
// Final newline after progress
|
||||
fmt.Printf("\n")
|
||||
log.Infof("\t> Local copy complete: %s\n", dstPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadPart downloads a byte-range [start,end] of the URL into a temporary part file.
|
||||
// On error, it returns the error; progress for the range is reported via pw.
|
||||
// The caller goroutine must call wg.Done() exactly once.
|
||||
func DownloadPart(urlStr string, start, end int64, partFileName string, pw *ProgressWriter, retryLimit int) error {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
// Retry loop for downloading a part
|
||||
for retries := 0; retries <= retryLimit; retries++ {
|
||||
req, errReq := http.NewRequest("GET", urlStr, nil)
|
||||
if errReq != nil {
|
||||
log.Errorf("Error creating request: %v", errReq)
|
||||
return errReq
|
||||
}
|
||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading part (attempt %d): %v", retries+1, err)
|
||||
// Exponential backoff before retry
|
||||
time.Sleep(time.Duration(1<<uint(retries)) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// If we get Partial Content, proceed to save
|
||||
if resp.StatusCode == http.StatusPartialContent {
|
||||
partFile, errCreate := os.Create(partFileName)
|
||||
if errCreate != nil {
|
||||
log.Errorf("Error creating part file: %v", errCreate)
|
||||
resp.Body.Close()
|
||||
return errCreate
|
||||
}
|
||||
// Copy with progress
|
||||
buf := make([]byte, 32*1024) // 32 KB buffer
|
||||
_, errCopy := io.CopyBuffer(io.MultiWriter(partFile, pw), resp.Body, buf)
|
||||
partFile.Close()
|
||||
resp.Body.Close()
|
||||
if errCopy != nil {
|
||||
log.Errorf("Error saving part: %v", errCopy)
|
||||
return errCopy
|
||||
}
|
||||
return nil // success
|
||||
}
|
||||
|
||||
// Unexpected status => retry
|
||||
log.Errorf("Error: expected status 206 Partial Content, got %d", resp.StatusCode)
|
||||
resp.Body.Close()
|
||||
time.Sleep(time.Duration(1<<uint(retries)) * time.Second)
|
||||
}
|
||||
|
||||
log.Errorf("Failed to download part after %d retries", retryLimit)
|
||||
return fmt.Errorf("failed to download part after %d retries", retryLimit)
|
||||
}
|
||||
|
||||
// DownloadVHDX downloads a VHD from a URL or copies from a local path if detected.
|
||||
// - urlStr: can be HTTP(S) URL or a local filesystem path (absolute or relative), or file:// URI.
|
||||
// - filePath: destination path to write the VHD.
|
||||
// - numParts: for HTTP downloads, number of parallel parts; ignored for local copy.
|
||||
// - retryLimit: retry count per part.
|
||||
//
|
||||
// It returns an error on failure.
|
||||
func DownloadVHDX(urlStr string, filePath string, numParts int, retryLimit int) error {
|
||||
// First, check if urlStr is a local file path or file:// URI.
|
||||
if u, err := url.Parse(urlStr); err == nil {
|
||||
if u.Scheme == "file" {
|
||||
// file:// URI: extract path
|
||||
localPath := u.Path
|
||||
// On Windows, file:///C:/path yields u.Path="/C:/path". Strip leading slash.
|
||||
if strings.HasPrefix(localPath, "/") && os.PathSeparator == '\\' && len(localPath) > 2 && localPath[1] == ':' {
|
||||
localPath = localPath[1:]
|
||||
}
|
||||
return copyLocalFile(localPath, filePath)
|
||||
}
|
||||
}
|
||||
// If no scheme or non-file scheme, check if it's a path existing on disk:
|
||||
if fi, err := os.Stat(urlStr); err == nil && !fi.IsDir() {
|
||||
// Treat as local file
|
||||
return copyLocalFile(urlStr, filePath)
|
||||
}
|
||||
|
||||
// Otherwise assume HTTP(S) URL:
|
||||
// First, HEAD to get total size
|
||||
resp, err := http.Head(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file info from URL %q: %w", urlStr, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status from HEAD %q: %s", urlStr, resp.Status)
|
||||
}
|
||||
totalSize := resp.ContentLength
|
||||
if totalSize <= 0 {
|
||||
return fmt.Errorf("unknown content length for URL %q", urlStr)
|
||||
}
|
||||
|
||||
// For progress display: use base name of destination
|
||||
pw := NewProgressWriter(totalSize, filepath.Base(filePath))
|
||||
|
||||
// Partition download into numParts
|
||||
partSize := totalSize / int64(numParts)
|
||||
var wg sync.WaitGroup
|
||||
var muErr sync.Mutex
|
||||
downloadErrors := make([]error, 0, numParts)
|
||||
partFiles := make([]string, numParts)
|
||||
|
||||
for i := 0; i < numParts; i++ {
|
||||
start := int64(i) * partSize
|
||||
end := start + partSize - 1
|
||||
if i == numParts-1 {
|
||||
end = totalSize - 1
|
||||
}
|
||||
partFileName := fmt.Sprintf("%s.part-%d.tmp", filePath, i)
|
||||
partFiles[i] = partFileName
|
||||
|
||||
wg.Add(1)
|
||||
go func(idx int, s, e int64, pfn string) {
|
||||
defer wg.Done()
|
||||
errPart := DownloadPart(urlStr, s, e, pfn, pw, retryLimit)
|
||||
if errPart != nil {
|
||||
muErr.Lock()
|
||||
downloadErrors = append(downloadErrors, fmt.Errorf("part %d: %w", idx, errPart))
|
||||
muErr.Unlock()
|
||||
}
|
||||
}(i, start, end, partFileName)
|
||||
}
|
||||
|
||||
// Wait for all parts
|
||||
wg.Wait()
|
||||
|
||||
if len(downloadErrors) > 0 {
|
||||
// Clean up partial files
|
||||
for _, pfn := range partFiles {
|
||||
os.Remove(pfn)
|
||||
}
|
||||
return fmt.Errorf("download failed for parts: %v", downloadErrors)
|
||||
}
|
||||
|
||||
// Merge parts
|
||||
outFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file %q: %w", filePath, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
for _, pfn := range partFiles {
|
||||
f, errOpen := os.Open(pfn)
|
||||
if errOpen != nil {
|
||||
return fmt.Errorf("failed to open part file %q: %w", pfn, errOpen)
|
||||
}
|
||||
_, errCopy := io.Copy(outFile, f)
|
||||
f.Close()
|
||||
if errCopy != nil {
|
||||
return fmt.Errorf("failed to merge part file %q: %w", pfn, errCopy)
|
||||
}
|
||||
os.Remove(pfn)
|
||||
}
|
||||
|
||||
// Final newline after progress
|
||||
fmt.Printf("\n")
|
||||
log.Infof("\t> Download complete: %s\n", filePath)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -25,8 +25,42 @@ import (
|
|||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"k8s.io/minikube/pkg/libmachine/log"
|
||||
)
|
||||
|
||||
type GuestUtil struct {
|
||||
os string
|
||||
vhdUrl string
|
||||
}
|
||||
|
||||
// ConfigGuest is the package-level singleton for GuestUtil
|
||||
var ConfigGuest *GuestUtil
|
||||
|
||||
func SetGuestUtil(guestOS, vhdUrl string) {
|
||||
ConfigGuest = &GuestUtil{
|
||||
os: guestOS,
|
||||
vhdUrl: vhdUrl,
|
||||
}
|
||||
log.Debugf("SetGuestUtil: os=%s, vhdUrl=%s", guestOS, vhdUrl)
|
||||
}
|
||||
|
||||
func (g *GuestUtil) GetGuestOS() string {
|
||||
if g == nil {
|
||||
log.Debugf("GuestUtil is not initialized")
|
||||
return "unknown"
|
||||
}
|
||||
return g.os
|
||||
}
|
||||
|
||||
func (g *GuestUtil) GetVHDUrl() string {
|
||||
if g == nil {
|
||||
log.Debugf("GuestUtil is not initialized")
|
||||
return ""
|
||||
}
|
||||
return g.vhdUrl
|
||||
}
|
||||
|
||||
type MultiError struct {
|
||||
Errs []error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package bootstrapper
|
|||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/minikube/pkg/libmachine/host"
|
||||
"k8s.io/minikube/pkg/minikube/bootstrapper/images"
|
||||
"k8s.io/minikube/pkg/minikube/config"
|
||||
"k8s.io/minikube/pkg/minikube/constants"
|
||||
|
|
@ -42,8 +43,11 @@ type Bootstrapper interface {
|
|||
UpdateCluster(config.ClusterConfig) error
|
||||
DeleteCluster(config.KubernetesConfig) error
|
||||
WaitForNode(config.ClusterConfig, config.Node, time.Duration) error
|
||||
SetupMinikubeCert(*host.Host) (string, error)
|
||||
JoinClusterWindows(*host.Host, config.ClusterConfig, config.Node, string, time.Duration) (string, error)
|
||||
JoinCluster(config.ClusterConfig, config.Node, string) error
|
||||
UpdateNode(config.ClusterConfig, config.Node, cruntime.Manager) error
|
||||
GenerateTokenWindows(config.ClusterConfig) (string, error)
|
||||
GenerateToken(config.ClusterConfig) (string, error)
|
||||
// LogCommands returns a map of log type to a command which will display that log.
|
||||
LogCommands(config.ClusterConfig, LogOptions) map[string]string
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ type sharedCACerts struct {
|
|||
|
||||
// SetupCerts gets the generated credentials required to talk to the APIServer.
|
||||
func SetupCerts(k8s config.ClusterConfig, n config.Node, pcpCmd command.Runner, cmd command.Runner) error {
|
||||
// no need to setup certs for windows worker nodes as the master node already took care of this
|
||||
if n.Guest.Name == "windows" {
|
||||
return nil
|
||||
}
|
||||
|
||||
localPath := localpath.Profile(k8s.KubernetesConfig.ClusterName)
|
||||
klog.Infof("Setting up %s for IP: %s", localPath, n.IP)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import (
|
|||
"k8s.io/minikube/pkg/drivers/kic/oci"
|
||||
"k8s.io/minikube/pkg/kapi"
|
||||
"k8s.io/minikube/pkg/libmachine"
|
||||
"k8s.io/minikube/pkg/libmachine/host"
|
||||
"k8s.io/minikube/pkg/libmachine/state"
|
||||
"k8s.io/minikube/pkg/minikube/assets"
|
||||
"k8s.io/minikube/pkg/minikube/bootstrapper"
|
||||
|
|
@ -751,6 +752,78 @@ func (k *Bootstrapper) restartPrimaryControlPlane(cfg config.ClusterConfig) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
|
||||
func (k *Bootstrapper) SetupMinikubeCert(host *host.Host) (string, error) {
|
||||
out.Step(style.Provisioning, "Setting up minikube certificates folder...")
|
||||
|
||||
certsDir := `C:\var\lib\minikube\certs`
|
||||
k8sPkiDir := `C:\etc\kubernetes\pki`
|
||||
caCert := `ca.crt`
|
||||
|
||||
script := fmt.Sprintf(
|
||||
`mkdir %s; `+
|
||||
`Copy-Item %s\%s -Destination %s; `+
|
||||
`Remove-Item %s\%s`,
|
||||
certsDir, k8sPkiDir, caCert, certsDir, k8sPkiDir, caCert,
|
||||
)
|
||||
|
||||
script = strings.ReplaceAll(script, `"`, `\"`)
|
||||
|
||||
command := fmt.Sprintf("powershell -NoProfile -NonInteractive -Command \"%s\"", script)
|
||||
klog.Infof("[executing] : %v", command)
|
||||
|
||||
host.RunSSHCommand(command)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (k *Bootstrapper) JoinClusterWindows(host *host.Host, cc config.ClusterConfig, n config.Node, joinCmd string, timeout time.Duration) (string, error) {
|
||||
setLocationPath := `Set-Location -Path "C:\k"`
|
||||
|
||||
psScript := fmt.Sprintf("%s; %s", setLocationPath, joinCmd)
|
||||
|
||||
psScript = strings.ReplaceAll(psScript, `"`, `\"`)
|
||||
|
||||
command := fmt.Sprintf("powershell -NoProfile -NonInteractive -Command \"%s\"", psScript)
|
||||
klog.Infof("[executing] : %v", command)
|
||||
|
||||
// TODO: Explore how to make this better; channels for result and errors for now exist
|
||||
resultChan := make(chan string, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
output, err := host.RunSSHCommand(command)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- output
|
||||
}()
|
||||
|
||||
if timeout > 0 {
|
||||
// If timeout is set, enforce it
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result, nil
|
||||
case err := <-errorChan:
|
||||
return "", err
|
||||
case <-time.After(timeout):
|
||||
return "", fmt.Errorf("operation timed out after %s", timeout)
|
||||
}
|
||||
} else {
|
||||
// If no timeout is set, just wait for result or error
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result, nil
|
||||
case err := <-errorChan:
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JoinCluster adds new node to an existing cluster.
|
||||
func (k *Bootstrapper) JoinCluster(cc config.ClusterConfig, n config.Node, joinCmd string) error {
|
||||
// Join the control plane by specifying its token
|
||||
|
|
@ -778,6 +851,29 @@ func (k *Bootstrapper) JoinCluster(cc config.ClusterConfig, n config.Node, joinC
|
|||
return nil
|
||||
}
|
||||
|
||||
// GenerateTokenWindows creates a token and returns the appropriate kubeadm join command to run, or the already existing token
|
||||
func (k *Bootstrapper) GenerateTokenWindows(cc config.ClusterConfig) (string, error) {
|
||||
tokenCmd := exec.Command("sudo", "/bin/bash", "-c", fmt.Sprintf("%s token create --print-join-command --ttl=0", bsutil.KubeadmCmdWithPath(cc.KubernetesConfig.KubernetesVersion)))
|
||||
r, err := k.c.RunCmd(tokenCmd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generating join command: %w", err)
|
||||
}
|
||||
|
||||
joinCmd := r.Stdout.String()
|
||||
// log the join command for debugging purposes
|
||||
klog.Infof("Generated join command ===: %s", joinCmd)
|
||||
joinCmd = strings.Replace(joinCmd, "kubeadm", ".\\kubeadm.exe", 1)
|
||||
joinCmd = fmt.Sprintf("%s --ignore-preflight-errors=all", strings.TrimSpace(joinCmd))
|
||||
|
||||
// append the cri-socket flag to the join command for windows
|
||||
joinCmd = fmt.Sprintf("%s --cri-socket \"npipe:////./pipe/containerd-containerd\"", joinCmd)
|
||||
|
||||
// append --v=5 to the join command for windows
|
||||
joinCmd = fmt.Sprintf("%s --v=5", joinCmd)
|
||||
|
||||
return joinCmd, nil
|
||||
}
|
||||
|
||||
// GenerateToken creates a token and returns the appropriate kubeadm join command to run, or the already existing token
|
||||
func (k *Bootstrapper) GenerateToken(cc config.ClusterConfig) (string, error) {
|
||||
// Take that generated token and use it to get a kubeadm join command
|
||||
|
|
@ -931,6 +1027,12 @@ func (k *Bootstrapper) UpdateCluster(cfg config.ClusterConfig) error {
|
|||
|
||||
// UpdateNode updates new or existing node.
|
||||
func (k *Bootstrapper) UpdateNode(cfg config.ClusterConfig, n config.Node, r cruntime.Manager) error {
|
||||
// skip if the node is a windows node
|
||||
if n.Guest.Name == "windows" {
|
||||
klog.Infof("skipping node %v update, as it is a windows node", n)
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.Infof("updating node %v ...", n)
|
||||
|
||||
kubeletCfg, err := bsutil.NewKubeletConfig(cfg, n, r)
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ type ClusterConfig struct {
|
|||
SSHAgentPID int
|
||||
GPUs string
|
||||
AutoPauseInterval time.Duration // Specifies interval of time to wait before checking if cluster should be paused
|
||||
WindowsNodeVersion string // OS version of windows node
|
||||
Rosetta bool // Only used by vfkit driver
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +150,7 @@ type Node struct {
|
|||
ContainerRuntime string
|
||||
ControlPlane bool
|
||||
Worker bool
|
||||
Guest Guest
|
||||
}
|
||||
|
||||
// VersionedExtraOption holds information on flags to apply to a specific range
|
||||
|
|
@ -182,3 +184,9 @@ type ScheduledStopConfig struct {
|
|||
InitiationTime int64
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type Guest struct {
|
||||
Name string
|
||||
Version string
|
||||
URL string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import (
|
|||
var (
|
||||
// SupportedArchitectures is the list of supported architectures
|
||||
SupportedArchitectures = [4]string{"amd64", "arm64", "ppc64le", "s390x"}
|
||||
// IP Address for the control plane
|
||||
MasterNodeIP = ""
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -175,6 +177,13 @@ const (
|
|||
|
||||
// Mirror CN
|
||||
AliyunMirror = "registry.cn-hangzhou.aliyuncs.com/google_containers"
|
||||
|
||||
// DefaultWindowsNodeVersion is the default version of Windows node
|
||||
DefaultWindowsNodeVersion = "2025"
|
||||
|
||||
// DefaultWindowsVhdURL is the VHD download URL for Windows Server 2025.
|
||||
// This will be used whenever the user does NOT supply --windows-vhd-url.
|
||||
DefaultWindowsVhdURL = "https://minikubevhdimagebuider.blob.core.windows.net/versions/hybrid-minikube-windows-server.vhdx"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ func downloadISO(isoURL string, skipChecksum bool) error {
|
|||
|
||||
// Lock before we check for existence to avoid thundering herd issues
|
||||
dst := localISOPath(u)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil {
|
||||
return fmt.Errorf("making cache image directory: %s: %w", dst, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,14 +91,19 @@ type LocalClient struct {
|
|||
commandOptions *run.CommandOptions
|
||||
}
|
||||
|
||||
// DefineGuest sets/tracks the guest OS for the host
|
||||
func (api *LocalClient) DefineGuest(h *host.Host) {
|
||||
api.legacyClient.DefineGuest(h)
|
||||
}
|
||||
|
||||
// NewHost creates a new Host
|
||||
func (api *LocalClient) NewHost(drvName string, rawDriver []byte) (*host.Host, error) {
|
||||
func (api *LocalClient) NewHost(drvName string, guest host.Guest, rawDriver []byte) (*host.Host, error) {
|
||||
def := registry.Driver(drvName)
|
||||
if def.Empty() {
|
||||
return nil, fmt.Errorf("driver %q does not exist", drvName)
|
||||
}
|
||||
if def.Init == nil {
|
||||
return api.legacyClient.NewHost(drvName, rawDriver)
|
||||
return api.legacyClient.NewHost(drvName, guest, rawDriver)
|
||||
}
|
||||
d := def.Init(api.commandOptions)
|
||||
err := json.Unmarshal(rawDriver, d)
|
||||
|
|
@ -111,6 +116,7 @@ func (api *LocalClient) NewHost(drvName string, rawDriver []byte) (*host.Host, e
|
|||
Name: d.GetMachineName(),
|
||||
Driver: d,
|
||||
DriverName: d.DriverName(),
|
||||
Guest: guest,
|
||||
HostOptions: &host.Options{
|
||||
AuthOptions: &auth.Options{
|
||||
CertDir: api.certsDir,
|
||||
|
|
@ -221,10 +227,14 @@ func (api *LocalClient) Create(h *host.Host) error {
|
|||
{
|
||||
"provisioning",
|
||||
func() error {
|
||||
// Skippable because we don't reconfigure Docker?
|
||||
// Skipped because we don't reconfigure Docker?
|
||||
if driver.BareMetal(h.Driver.DriverName()) {
|
||||
return nil
|
||||
}
|
||||
// Skipped because we don't reconfigure Docker for Windows Host
|
||||
if h.Guest.Name == "windows" {
|
||||
return nil
|
||||
}
|
||||
return provisionDockerMachine(h)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"k8s.io/minikube/pkg/libmachine/drivers/plugin/localbinary"
|
||||
"k8s.io/minikube/pkg/libmachine/host"
|
||||
|
||||
"k8s.io/minikube/pkg/minikube/driver"
|
||||
_ "k8s.io/minikube/pkg/minikube/registry/drvs/virtualbox"
|
||||
|
|
@ -71,19 +72,30 @@ func TestLocalClientNewHost(t *testing.T) {
|
|||
var tests = []struct {
|
||||
description string
|
||||
driver string
|
||||
guest host.Guest
|
||||
rawDriver []byte
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
description: "host vbox correct",
|
||||
driver: driver.VirtualBox,
|
||||
rawDriver: []byte(vboxConfig),
|
||||
guest: host.Guest{
|
||||
Name: "linux",
|
||||
Version: "1.0.0",
|
||||
URL: "https://example.com/linux.iso",
|
||||
},
|
||||
rawDriver: []byte(vboxConfig),
|
||||
},
|
||||
{
|
||||
description: "host vbox incorrect",
|
||||
driver: driver.VirtualBox,
|
||||
rawDriver: []byte("?"),
|
||||
err: true,
|
||||
guest: host.Guest{
|
||||
Name: "linux",
|
||||
Version: "1.0.0",
|
||||
URL: "https://example.com/linux.iso",
|
||||
},
|
||||
rawDriver: []byte("?"),
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +103,7 @@ func TestLocalClientNewHost(t *testing.T) {
|
|||
test := test
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
host, err := c.NewHost(test.driver, test.rawDriver)
|
||||
host, err := c.NewHost(test.driver, test.guest, test.rawDriver)
|
||||
// A few sanity checks that we can do on the host
|
||||
if host != nil {
|
||||
if host.DriverName != test.driver {
|
||||
|
|
@ -100,6 +112,9 @@ func TestLocalClientNewHost(t *testing.T) {
|
|||
if host.Name != host.Driver.GetMachineName() {
|
||||
t.Errorf("Host name is not correct. Expected :%s, got: %s", host.Driver.GetMachineName(), host.Name)
|
||||
}
|
||||
if host.Guest.Name != test.guest.Name {
|
||||
t.Errorf("Host's guest os is not correct. Expected :%s, got: %s", test.guest.Name, host.Guest.Name)
|
||||
}
|
||||
}
|
||||
if err != nil && !test.err {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ func createHost(api libmachine.API, cfg *config.ClusterConfig, n *config.Node) (
|
|||
return nil, fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
h, err := api.NewHost(cfg.Driver, data)
|
||||
h, err := api.NewHost(cfg.Driver, host.Guest(n.Guest), data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new host: %w", err)
|
||||
}
|
||||
|
|
@ -154,11 +154,14 @@ func createHost(api libmachine.API, cfg *config.ClusterConfig, n *config.Node) (
|
|||
h.HostOptions.AuthOptions.StorePath = localpath.MiniPath()
|
||||
h.HostOptions.EngineOptions = engineOptions(*cfg)
|
||||
|
||||
api.DefineGuest(h)
|
||||
|
||||
cstart := time.Now()
|
||||
klog.Infof("libmachine.API.Create for %q (driver=%q)", cfg.Name, cfg.Driver)
|
||||
|
||||
if cfg.StartHostTimeout == 0 {
|
||||
cfg.StartHostTimeout = 6 * time.Minute
|
||||
// increase default wait to accommodate slower hosts and Windows nodes
|
||||
cfg.StartHostTimeout = 12 * time.Minute
|
||||
}
|
||||
if err := timedCreateHost(h, api, cfg.StartHostTimeout); err != nil {
|
||||
return nil, fmt.Errorf("creating host: %w", err)
|
||||
|
|
@ -182,6 +185,7 @@ func timedCreateHost(h *host.Host, api libmachine.API, t time.Duration) error {
|
|||
create := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(create)
|
||||
klog.Infof("libmachine.API.Create starting for %q (GuestOS=%q)", h.Name, h.Guest.Name)
|
||||
create <- api.Create(h)
|
||||
}()
|
||||
|
||||
|
|
@ -299,6 +303,12 @@ func postStartSetup(h *host.Host, mc config.ClusterConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// skip postStartSetup for windows guest os
|
||||
if h.Guest.Name == "windows" {
|
||||
klog.Infof("skipping postStartSetup for windows guest os")
|
||||
return nil
|
||||
}
|
||||
|
||||
k8sVer, err := semver.ParseTolerant(mc.KubernetesConfig.KubernetesVersion)
|
||||
if err != nil {
|
||||
klog.Errorf("unable to parse Kubernetes version: %s", mc.KubernetesConfig.KubernetesVersion)
|
||||
|
|
@ -426,3 +436,26 @@ func addHostAliasCommand(name string, record string, sudo bool, destPath string)
|
|||
destPath)
|
||||
return exec.Command("/bin/bash", "-c", script)
|
||||
}
|
||||
|
||||
func AddHostAliasWindows(host *host.Host, controlPlaneIP string) (string, error) {
|
||||
out.Step(style.Provisioning, "Adding host alias for control plane ...")
|
||||
|
||||
path := "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||||
entry := fmt.Sprintf("\t%s\tcontrol-plane.minikube.internal", controlPlaneIP)
|
||||
|
||||
psScript := fmt.Sprintf(
|
||||
`$hostsContent = Get-Content -Path "%s" -Raw -ErrorAction SilentlyContinue; `+
|
||||
`if ($hostsContent -notmatch [regex]::Escape("%s")) { `+
|
||||
`Add-Content -Path "%s" -Value "%s" -Force | Out-Null }`,
|
||||
path, entry, path, entry,
|
||||
)
|
||||
|
||||
psScript = strings.ReplaceAll(psScript, `"`, `\"`)
|
||||
|
||||
command := fmt.Sprintf("powershell -NoProfile -NonInteractive -Command \"%s\"", psScript)
|
||||
klog.Infof("[executing] : %v", command)
|
||||
|
||||
host.RunSSHCommand(command)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ func stop(h *host.Host) error {
|
|||
}
|
||||
|
||||
if driver.NeedsShutdown(h.DriverName) {
|
||||
klog.Infof("GuestOS: %s", h.Guest.Name)
|
||||
if err := trySSHPowerOff(h); err != nil {
|
||||
return fmt.Errorf("ssh power off: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
Copyright 2026 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 node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var powershell string
|
||||
|
||||
var (
|
||||
ErrPowerShellNotFound = errors.New("powershell was not found in the path")
|
||||
ErrNotAdministrator = errors.New("hyper-v commands have to be run as an Administrator")
|
||||
ErrNotInstalled = errors.New("hyper-V PowerShell Module is not available")
|
||||
)
|
||||
|
||||
func init() {
|
||||
powershell, _ = exec.LookPath("powershell.exe")
|
||||
}
|
||||
|
||||
func cmdOut(args ...string) (string, error) {
|
||||
args = append([]string{"-NoProfile", "-NonInteractive"}, args...)
|
||||
cmd := exec.Command(powershell, args...)
|
||||
klog.Infof("[executing ==>] : %v %v", powershell, strings.Join(args, " "))
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
klog.Infof("[stdout =====>] : %s", stdout.String())
|
||||
klog.Infof("[stderr =====>] : %s", stderr.String())
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
||||
func cmd(args ...string) error {
|
||||
_, err := cmdOut(args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func CmdOutSSH(client *ssh.Client, script string) (string, error) {
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
command := fmt.Sprintf("powershell -NoProfile -NonInteractive -Command \"%s\"", script)
|
||||
klog.Infof("[executing] : %v", command)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
session.Stdout = &stdout
|
||||
session.Stderr = &stderr
|
||||
|
||||
err = session.Run(command)
|
||||
klog.Infof("[stdout =====>] : %s", stdout.String())
|
||||
klog.Infof("[stderr =====>] : %s", stderr.String())
|
||||
return stdout.String(), err
|
||||
}
|
||||
|
|
@ -110,35 +110,60 @@ func Start(starter Starter, options *run.CommandOptions) (*kubeconfig.Settings,
|
|||
return nil, config.Write(viper.GetString(config.ProfileName), starter.Cfg)
|
||||
}
|
||||
|
||||
// wait for preloaded tarball to finish downloading before configuring runtimes
|
||||
waitCacheRequiredImages(&cacheGroup)
|
||||
// log starter.Node.OS here
|
||||
klog.Infof("Node OS: %s", starter.Node.Guest.Name)
|
||||
if starter.Node.Guest.Name != "windows" {
|
||||
// wait for preloaded tarball to finish downloading before configuring runtimes
|
||||
waitCacheRequiredImages(&cacheGroup)
|
||||
}
|
||||
|
||||
sv, err := util.ParseKubernetesVersion(starter.Node.KubernetesVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse Kubernetes version: %w", err)
|
||||
}
|
||||
klog.Infof("Kubernetes version: %s", sv)
|
||||
|
||||
// configure the runtime (docker, containerd, crio)
|
||||
cr := configureRuntimes(starter.Runner, *starter.Cfg, sv)
|
||||
var cr cruntime.Manager
|
||||
if starter.Node.Guest.Name != "windows" {
|
||||
// configure the runtime (docker, containerd, crio) only for windows nodes
|
||||
cr = configureRuntimes(starter.Runner, *starter.Cfg, sv)
|
||||
|
||||
// check if installed runtime is compatible with current minikube code
|
||||
if err = cruntime.CheckCompatibility(cr); err != nil {
|
||||
return nil, err
|
||||
// check if installed runtime is compatible with current minikube code
|
||||
if err = cruntime.CheckCompatibility(cr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
showVersionInfo(starter.Node.KubernetesVersion, cr)
|
||||
}
|
||||
|
||||
showVersionInfo(starter.Node.KubernetesVersion, cr)
|
||||
klog.Infof("configureRuntimes done: cr=%v", cr)
|
||||
|
||||
// add "host.minikube.internal" dns alias (intentionally non-fatal)
|
||||
hostIP, err := cluster.HostIP(starter.Host, starter.Cfg.Name)
|
||||
if err != nil {
|
||||
klog.Errorf("Unable to get host IP: %v", err)
|
||||
} else if err := machine.AddHostAlias(starter.Runner, constants.HostAlias, hostIP); err != nil {
|
||||
klog.Errorf("Unable to add minikube host alias: %v", err)
|
||||
}
|
||||
|
||||
if starter.Node.Guest.Name != "windows" {
|
||||
if err := machine.AddHostAlias(starter.Runner, constants.HostAlias, hostIP); err != nil {
|
||||
klog.Warningf("Unable to add host alias: %v", err)
|
||||
}
|
||||
} else {
|
||||
out.Step(style.Provisioning, "Configuring Windows node...")
|
||||
if stdout, err := machine.AddHostAliasWindows(starter.Host, constants.MasterNodeIP); err != nil {
|
||||
klog.Warningf("Unable to add host alias: %v", err)
|
||||
} else {
|
||||
klog.Infof("Host alias added: %s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
var kcs *kubeconfig.Settings
|
||||
var bs bootstrapper.Bootstrapper
|
||||
if config.IsPrimaryControlPlane(*starter.Cfg, *starter.Node) {
|
||||
constants.MasterNodeIP, err = starter.Host.Driver.GetIP()
|
||||
if err != nil {
|
||||
klog.Errorf("Unable to get driver IP: %v", err)
|
||||
}
|
||||
klog.Infof("Driver IP: %s", constants.MasterNodeIP)
|
||||
// [re]start primary control-plane node
|
||||
kcs, bs, err = startPrimaryControlPlane(starter, cr, options)
|
||||
if err != nil {
|
||||
|
|
@ -253,6 +278,30 @@ func Start(starter Starter, options *run.CommandOptions) (*kubeconfig.Settings,
|
|||
addons.UpdateConfigToDisable(starter.Cfg, options)
|
||||
}
|
||||
|
||||
// for windows node prepare the linux control plane node for windows-specific flannel CNI config
|
||||
if config.IsPrimaryControlPlane(*starter.Cfg, *starter.Node) && starter.Cfg.WindowsNodeVersion == "2022" {
|
||||
if err := prepareLinuxNode(starter.Runner); err != nil {
|
||||
klog.Errorf("Failed to prepare Linux node for Windows-specific Flannel CNI config: %v", err)
|
||||
}
|
||||
|
||||
// set up flannel network issues
|
||||
if err := configureFlannelCNI(); err != nil {
|
||||
klog.Errorf("error configuring flannel CNI: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// for windows node prepare the linux control plane node for windows-specific flannel CNI config
|
||||
if config.IsPrimaryControlPlane(*starter.Cfg, *starter.Node) && starter.Cfg.WindowsNodeVersion == "2022" {
|
||||
if err := prepareLinuxNode(starter.Runner); err != nil {
|
||||
klog.Errorf("Failed to prepare Linux node for Windows-specific Flannel CNI config: %v", err)
|
||||
}
|
||||
|
||||
// set up flannel network issues
|
||||
if err := configureFlannelCNI(); err != nil {
|
||||
klog.Errorf("error configuring flannel CNI: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write enabled addons to the config before completion
|
||||
klog.Infof("writing updated cluster config ...")
|
||||
return kcs, config.Write(viper.GetString(config.ProfileName), starter.Cfg)
|
||||
|
|
@ -317,6 +366,7 @@ func startPrimaryControlPlane(starter Starter, cr cruntime.Manager, options *run
|
|||
func joinCluster(starter Starter, cpBs bootstrapper.Bootstrapper, bs bootstrapper.Bootstrapper, options *run.CommandOptions) error {
|
||||
start := time.Now()
|
||||
klog.Infof("joinCluster: %+v", starter.Cfg)
|
||||
out.Step(style.Waiting, "Joining {{.name}} to the cluster", out.V{"name": starter.Node.Name})
|
||||
defer func() {
|
||||
klog.Infof("duration metric: took %s to joinCluster", time.Since(start))
|
||||
}()
|
||||
|
|
@ -336,35 +386,93 @@ func joinCluster(starter Starter, cpBs bootstrapper.Bootstrapper, bs bootstrappe
|
|||
klog.Infof("successfully removed existing %s node %q from cluster: %+v", role, starter.Node.Name, starter.Node)
|
||||
}
|
||||
|
||||
joinCmd, err := cpBs.GenerateToken(*starter.Cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating join token: %w", err)
|
||||
// declare joinCmd variable
|
||||
var joinCmd string
|
||||
var err error
|
||||
|
||||
// if node is a windows node, generate the join command
|
||||
if starter.Node.Guest.Name == "windows" {
|
||||
joinCmd, err = cpBs.GenerateTokenWindows(*starter.Cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating join token: %w", err)
|
||||
}
|
||||
} else {
|
||||
joinCmd, err = cpBs.GenerateToken(*starter.Cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating join token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
klog.Infof("join command: %s", joinCmd)
|
||||
|
||||
join := func() error {
|
||||
klog.Infof("trying to join %s node %q to cluster: %+v", role, starter.Node.Name, starter.Node)
|
||||
if err := bs.JoinCluster(*starter.Cfg, *starter.Node, joinCmd); err != nil {
|
||||
klog.Errorf("%s node failed to join cluster, will retry: %v", role, err)
|
||||
if starter.Node.Guest.Name != "windows" {
|
||||
if err := bs.JoinCluster(*starter.Cfg, *starter.Node, joinCmd); err != nil {
|
||||
// log the error message and retry
|
||||
klog.Errorf("%s node failed to join cluster, will retry: %v", role, err)
|
||||
|
||||
// reset node to revert any changes made by previous kubeadm init/join
|
||||
klog.Infof("resetting %s node %q before attempting to rejoin cluster...", role, starter.Node.Name)
|
||||
if _, err := starter.Runner.RunCmd(exec.Command("sudo", "/bin/bash", "-c", fmt.Sprintf("%s reset --force", bsutil.KubeadmCmdWithPath(starter.Cfg.KubernetesConfig.KubernetesVersion)))); err != nil {
|
||||
klog.Infof("kubeadm reset failed, continuing anyway: %v", err)
|
||||
} else {
|
||||
klog.Infof("successfully reset %s node %q", role, starter.Node.Name)
|
||||
// reset node to revert any changes made by previous kubeadm init/join
|
||||
klog.Infof("resetting %s node %q before attempting to rejoin cluster...", role, starter.Node.Name)
|
||||
if _, err := starter.Runner.RunCmd(exec.Command("sudo", "/bin/bash", "-c", fmt.Sprintf("%s reset --force", bsutil.KubeadmCmdWithPath(starter.Cfg.KubernetesConfig.KubernetesVersion)))); err != nil {
|
||||
klog.Infof("kubeadm reset failed, continuing anyway: %v", err)
|
||||
} else {
|
||||
klog.Infof("successfully reset %s node %q", role, starter.Node.Name)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
driverIP, err := starter.Host.Driver.GetIP()
|
||||
if err != nil {
|
||||
klog.Errorf("Unable to get driver IP: %v", err)
|
||||
}
|
||||
klog.Infof("Driver IP: %s", driverIP)
|
||||
|
||||
return err
|
||||
// Call with a timeout of 30 seconds
|
||||
timeout := 20 * time.Second
|
||||
|
||||
if commandResult, err := bs.JoinClusterWindows(starter.Host, *starter.Cfg, *starter.Node, joinCmd, timeout); err != nil {
|
||||
klog.Infof("%s node failed to join cluster, will retry: %v", role, err)
|
||||
klog.Infof("command result: %s", commandResult)
|
||||
|
||||
// sort out the certificates issues
|
||||
if cmd, err := bs.SetupMinikubeCert(starter.Host); err != nil {
|
||||
klog.Errorf("error setting minikube folder error script: %v", err)
|
||||
} else {
|
||||
klog.Infof("command result: %s", cmd)
|
||||
// retry the join command
|
||||
if commandResult, err := bs.JoinClusterWindows(starter.Host, *starter.Cfg, *starter.Node, joinCmd, 0); err != nil {
|
||||
klog.Errorf("error retrying join command: %v, command result: %s", err, commandResult)
|
||||
return err
|
||||
}
|
||||
|
||||
// set up flannel network issues
|
||||
if err := prepareWindowsNodeFlannel(); err != nil {
|
||||
klog.Errorf("error preparing windows node flannel: %v", err)
|
||||
}
|
||||
|
||||
// set up kube-proxy issues
|
||||
if err := prepareWindowsNodeKubeProxy(); err != nil {
|
||||
klog.Errorf("error preparing windows node kube-proxy: %v", err)
|
||||
}
|
||||
}
|
||||
// return err
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := retry.Expo(join, 10*time.Second, 3*time.Minute); err != nil {
|
||||
return fmt.Errorf("error joining %s node %q to cluster: %w", role, starter.Node.Name, err)
|
||||
if starter.Node.Guest.Name != "windows" {
|
||||
return fmt.Errorf("error joining %s node %q to cluster: %w", role, starter.Node.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cpBs.LabelAndUntaintNode(*starter.Cfg, *starter.Node); err != nil {
|
||||
return fmt.Errorf("error applying %s node %q label: %w", role, starter.Node.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -679,11 +787,15 @@ func startMachine(cfg *config.ClusterConfig, node *config.Node, delOnFail bool,
|
|||
if err != nil {
|
||||
return runner, preExists, m, hostInfo, fmt.Errorf("Failed to get command runner: %w", err)
|
||||
}
|
||||
// log that we managed to get a command runner
|
||||
klog.Infof("CommandRunner returned: %v", runner)
|
||||
|
||||
ip, err := validateNetwork(hostInfo, runner, cfg.KubernetesConfig.ImageRepository)
|
||||
if err != nil {
|
||||
return runner, preExists, m, hostInfo, fmt.Errorf("Failed to validate network: %w", err)
|
||||
}
|
||||
// log that we managed to validate the network
|
||||
klog.Infof("validateNetwork returned: %v", ip)
|
||||
|
||||
if driver.IsQEMU(hostInfo.Driver.DriverName()) && network.IsBuiltinQEMU(cfg.Network) {
|
||||
apiServerPort, err := getPort()
|
||||
|
|
@ -699,6 +811,12 @@ func startMachine(cfg *config.ClusterConfig, node *config.Node, delOnFail bool,
|
|||
out.FailureT("Failed to set NO_PROXY Env. Please use `export NO_PROXY=$NO_PROXY,{{.ip}}`.", out.V{"ip": ip})
|
||||
}
|
||||
|
||||
// log that we managed to exclude the IP from the proxy
|
||||
klog.Infof("Excluded IP from proxy: %v", ip)
|
||||
|
||||
// log the result of the function
|
||||
klog.Infof("startMachine returned: %v, %v, %v, %v", runner, preExists, m, hostInfo)
|
||||
|
||||
return runner, preExists, m, hostInfo, err
|
||||
}
|
||||
|
||||
|
|
@ -942,6 +1060,52 @@ func prepareNone() {
|
|||
}
|
||||
}
|
||||
|
||||
func configureFlannelCNI() error {
|
||||
err := cmd("kubectl apply -f https://raw.githubusercontent.com/vrapolinario/MinikubeWindowsContainers/main/kube-flannel.yaml")
|
||||
if err != nil {
|
||||
klog.Errorf("failed to apply kube-flannel configuration: %v\n", err)
|
||||
}
|
||||
|
||||
roll_err := cmd("kubectl rollout restart ds kube-flannel-ds -n kube-flannel")
|
||||
if roll_err != nil {
|
||||
klog.Errorf("failed to restart kube-flannel daemonset: %v\n", roll_err)
|
||||
}
|
||||
klog.Infof("Successfully applied the configuration.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare windows node by flannel configuration
|
||||
func prepareWindowsNodeFlannel() error {
|
||||
err := cmd("kubectl apply -f https://raw.githubusercontent.com/vrapolinario/MinikubeWindowsContainers/main/flannel-overlay.yaml")
|
||||
if err != nil {
|
||||
klog.Errorf("failed to apply flannel configuration: %v\n", err)
|
||||
}
|
||||
klog.Infof("Successfully applied the configuration.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare linux nodes for Windows-specific Flannel CNI config
|
||||
func prepareLinuxNode(runner command.Runner) error {
|
||||
c := exec.Command("sudo", "sysctl", "net.bridge.bridge-nf-call-iptables=1")
|
||||
if rr, err := runner.RunCmd(c); err != nil {
|
||||
klog.Infof("couldn't run %q command. error: %v", rr.Command(), err)
|
||||
}
|
||||
// log that we managed to run the command
|
||||
klog.Infof("Successfully ran the command.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare windows node kube-proxy yaml configuration
|
||||
func prepareWindowsNodeKubeProxy() error {
|
||||
err := cmd("kubectl apply -f https://raw.githubusercontent.com/vrapolinario/MinikubeWindowsContainers/main/kube-proxy.yaml")
|
||||
if err != nil {
|
||||
klog.Errorf("failed to apply kube-proxy configuration: %v\n", err)
|
||||
}
|
||||
klog.Infof("Successfully applied the configuration.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// addCoreDNSEntry adds host name and IP record to the DNS by updating CoreDNS's ConfigMap.
|
||||
// ref: https://coredns.io/plugins/hosts/
|
||||
// note: there can be only one 'hosts' block in CoreDNS's ConfigMap (avoid "plugin/hosts: this plugin can only be used once per Server Block" error)
|
||||
|
|
@ -1021,3 +1185,15 @@ To see benchmarks checkout https://minikube.sigs.k8s.io/docs/benchmarks/cpuusage
|
|||
`, out.V{"drivers": altDriverList.String()})
|
||||
}
|
||||
}
|
||||
|
||||
// ValidWindowsOSVersions lists the supported Windows OS versions
|
||||
func ValidWindowsOSVersions() map[string]bool {
|
||||
// TODO: add more versions as they are tested and supported
|
||||
// return map[string]bool{"2019": true, "2022": true, "2025": true}
|
||||
return map[string]bool{"2025": true}
|
||||
}
|
||||
|
||||
// ValidOS lists the supported OSes
|
||||
func ValidOS() []string {
|
||||
return []string{"linux", "windows"}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,13 @@ func (api *MockAPI) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DefineGuest sets/tracks the guest OS for the host
|
||||
func (api *MockAPI) DefineGuest(h *host.Host) {
|
||||
api.Logf("MockAPI.DefineGuest: guest=%q", h.Guest.Name)
|
||||
}
|
||||
|
||||
// NewHost creates a new host.Host instance.
|
||||
func (api *MockAPI) NewHost(drvName string, rawDriver []byte) (*host.Host, error) {
|
||||
func (api *MockAPI) NewHost(drvName string, guest host.Guest, rawDriver []byte) (*host.Host, error) {
|
||||
var driver MockDriver
|
||||
if err := json.Unmarshal(rawDriver, &driver); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling json: %w", err)
|
||||
|
|
@ -81,6 +86,7 @@ func (api *MockAPI) NewHost(drvName string, rawDriver []byte) (*host.Host, error
|
|||
RawDriver: rawDriver,
|
||||
Driver: &MockDriver{},
|
||||
Name: fmt.Sprintf("mock-machine-%.8f", rand.Float64()),
|
||||
Guest: guest,
|
||||
HostOptions: &host.Options{
|
||||
AuthOptions: &auth.Options{},
|
||||
SwarmOptions: &swarm.Options{},
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ minikube start [flags]
|
|||
--vm Filter to use only VM Drivers
|
||||
--wait strings comma separated list of Kubernetes components to verify and wait for after starting a cluster. defaults to "apiserver,system_pods", available options: "apiserver,system_pods,default_sa,apps_running,node_ready,kubelet,extra" . other acceptable values are 'all' or 'none', 'true' and 'false' (default [apiserver,system_pods])
|
||||
--wait-timeout duration max time to wait per Kubernetes or host to be healthy. (default 6m0s)
|
||||
--windows-node-version string The version of Windows to use for the windows node on a multi-node cluster (e.g., 2025). Currently support Windows Server 2025
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
|
|
|
|||
Loading…
Reference in New Issue