Skip to content

Commit c4800a6

Browse files
committed
Make the default container runtime dynamic
Since the dockershim removal, there is no longer a constant default container runtime provided by upstream. Only CRI.
1 parent 25d17c2 commit c4800a6

File tree

14 files changed

+88
-26
lines changed

14 files changed

+88
-26
lines changed

cmd/minikube/cmd/docker-env.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ var dockerEnvCmd = &cobra.Command{
280280
exit.Message(reason.EnvMultiConflict, `The docker-env command is incompatible with multi-node clusters. Use the 'registry' add-on: https://minikube.sigs.k8s.io/docs/handbook/registry/`)
281281
}
282282

283-
if co.Config.KubernetesConfig.ContainerRuntime != "docker" {
283+
if co.Config.KubernetesConfig.ContainerRuntime != constants.Docker {
284284
exit.Message(reason.Usage, `The docker-env command is only compatible with the "docker" runtime, but this cluster was configured to use the "{{.runtime}}" runtime.`,
285285
out.V{"runtime": co.Config.KubernetesConfig.ContainerRuntime})
286286
}

cmd/minikube/cmd/podman-env.go

+5
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ var podmanEnvCmd = &cobra.Command{
170170
exit.Message(reason.Usage, `The podman-env command is incompatible with multi-node clusters. Use the 'registry' add-on: https://minikube.sigs.k8s.io/docs/handbook/registry/`)
171171
}
172172

173+
if co.Config.KubernetesConfig.ContainerRuntime != constants.CRIO {
174+
exit.Message(reason.Usage, `The podman-env command is only compatible with the "crio" runtime, but this cluster was configured to use the "{{.runtime}}" runtime.`,
175+
out.V{"runtime": co.Config.KubernetesConfig.ContainerRuntime})
176+
}
177+
173178
r := co.CP.Runner
174179
if ok := isPodmanAvailable(r); !ok {
175180
exit.Message(reason.EnvPodmanUnavailable, `The podman service within '{{.cluster}}' is not active`, out.V{"cluster": cname})

cmd/minikube/cmd/start.go

+53-9
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ func runStart(cmd *cobra.Command, args []string) {
188188

189189
validateSpecifiedDriver(existing)
190190
validateKubernetesVersion(existing)
191+
validateContainerRuntime(existing)
191192

192193
ds, alts, specified := selectDriver(existing)
193194
if cmd.Flag(kicBaseImage).Changed {
@@ -270,7 +271,7 @@ func runStart(cmd *cobra.Command, args []string) {
270271
exit.Error(reason.GuestStart, "failed to start node", err)
271272
}
272273

273-
if err := showKubectlInfo(kubeconfig, starter.Node.KubernetesVersion, starter.Cfg.Name); err != nil {
274+
if err := showKubectlInfo(kubeconfig, starter.Node.KubernetesVersion, starter.Node.ContainerRuntime, starter.Cfg.Name); err != nil {
274275
klog.Errorf("kubectl info: %v", err)
275276
}
276277
}
@@ -302,7 +303,8 @@ func provisionWithDriver(cmd *cobra.Command, ds registry.DriverState, existing *
302303
}
303304

304305
k8sVersion := getKubernetesVersion(existing)
305-
cc, n, err := generateClusterConfig(cmd, existing, k8sVersion, driverName)
306+
rtime := getContainerRuntime(existing)
307+
cc, n, err := generateClusterConfig(cmd, existing, k8sVersion, rtime, driverName)
306308
if err != nil {
307309
return node.Starter{}, errors.Wrap(err, "Failed to generate config")
308310
}
@@ -381,6 +383,7 @@ func startWithDriver(cmd *cobra.Command, starter node.Starter, existing *config.
381383
Worker: true,
382384
ControlPlane: false,
383385
KubernetesVersion: starter.Cfg.KubernetesConfig.KubernetesVersion,
386+
ContainerRuntime: starter.Cfg.KubernetesConfig.ContainerRuntime,
384387
}
385388
out.Ln("") // extra newline for clarity on the command line
386389
err := node.Add(starter.Cfg, n, viper.GetBool(deleteOnFailure))
@@ -439,15 +442,15 @@ func displayEnviron(env []string) {
439442
}
440443
}
441444

442-
func showKubectlInfo(kcs *kubeconfig.Settings, k8sVersion, machineName string) error {
445+
func showKubectlInfo(kcs *kubeconfig.Settings, k8sVersion, rtime, machineName string) error {
443446
if k8sVersion == constants.NoKubernetesVersion {
444447
register.Reg.SetStep(register.Done)
445448
out.Step(style.Ready, "Done! minikube is ready without Kubernetes!")
446449

447450
// Runtime message.
448451
boxConfig := box.Config{Py: 1, Px: 4, Type: "Round", Color: "Green"}
449-
switch viper.GetString(containerRuntime) {
450-
case constants.DefaultContainerRuntime:
452+
switch rtime {
453+
case constants.Docker:
451454
out.BoxedWithConfig(boxConfig, style.Tip, "Things to try without Kubernetes ...", `- "minikube ssh" to SSH into minikube's node.
452455
- "minikube docker-env" to point your docker-cli to the docker inside minikube.
453456
- "minikube image" to build images without docker.`)
@@ -1183,9 +1186,10 @@ func validateFlags(cmd *cobra.Command, drvName string) {
11831186
exit.Message(reason.DrvUnsupportedProfile, "The '{{.name}} driver does not support multiple profiles: https://minikube.sigs.k8s.io/docs/reference/drivers/none/", out.V{"name": drvName})
11841187
}
11851188

1186-
runtime := viper.GetString(containerRuntime)
1187-
if runtime != "docker" {
1188-
out.WarningT("Using the '{{.runtime}}' runtime with the 'none' driver is an untested configuration!", out.V{"runtime": runtime})
1189+
// default container runtime varies, starting with Kubernetes 1.24 - assume that only the default container runtime has been tested
1190+
rtime := viper.GetString(containerRuntime)
1191+
if rtime != constants.DefaultContainerRuntime && rtime != defaultRuntime(getKubernetesVersion(nil)) {
1192+
out.WarningT("Using the '{{.runtime}}' runtime with the 'none' driver is an untested configuration!", out.V{"runtime": rtime})
11891193
}
11901194

11911195
// conntrack is required starting with Kubernetes 1.18, include the release candidates for completion
@@ -1285,6 +1289,10 @@ func validateRuntime(rtime string) error {
12851289
// `crio` is accepted as an alternative spelling to `cri-o`
12861290
validOptions = append(validOptions, constants.CRIO)
12871291

1292+
if rtime == constants.DefaultContainerRuntime {
1293+
return nil
1294+
}
1295+
12881296
var validRuntime bool
12891297
for _, option := range validOptions {
12901298
if rtime == option {
@@ -1308,9 +1316,31 @@ func validateRuntime(rtime string) error {
13081316
return nil
13091317
}
13101318

1319+
func getContainerRuntime(old *config.ClusterConfig) string {
1320+
paramRuntime := viper.GetString(containerRuntime)
1321+
1322+
// try to load the old version first if the user didn't specify anything
1323+
if paramRuntime == constants.DefaultContainerRuntime && old != nil {
1324+
paramRuntime = old.KubernetesConfig.ContainerRuntime
1325+
}
1326+
1327+
if paramRuntime == constants.DefaultContainerRuntime {
1328+
k8sVersion := getKubernetesVersion(old)
1329+
paramRuntime = defaultRuntime(k8sVersion)
1330+
}
1331+
1332+
return paramRuntime
1333+
}
1334+
1335+
// defaultRuntime returns the default container runtime
1336+
func defaultRuntime(k8sVersion string) string {
1337+
// minikube default
1338+
return constants.Docker
1339+
}
1340+
13111341
// if container runtime is not docker, check that cni is not disabled
13121342
func validateCNI(cmd *cobra.Command, runtime string) {
1313-
if runtime == "docker" {
1343+
if runtime == constants.Docker {
13141344
return
13151345
}
13161346
if cmd.Flags().Changed(cniFlag) && strings.ToLower(viper.GetString(cniFlag)) == "false" {
@@ -1458,6 +1488,7 @@ func createNode(cc config.ClusterConfig, kubeNodeName string, existing *config.C
14581488
if existing != nil {
14591489
cp, err := config.PrimaryControlPlane(existing)
14601490
cp.KubernetesVersion = getKubernetesVersion(&cc)
1491+
cp.ContainerRuntime = getContainerRuntime(&cc)
14611492
if err != nil {
14621493
return cc, config.Node{}, err
14631494
}
@@ -1467,6 +1498,7 @@ func createNode(cc config.ClusterConfig, kubeNodeName string, existing *config.C
14671498
nodes := []config.Node{}
14681499
for _, n := range existing.Nodes {
14691500
n.KubernetesVersion = getKubernetesVersion(&cc)
1501+
n.ContainerRuntime = getContainerRuntime(&cc)
14701502
nodes = append(nodes, n)
14711503
}
14721504
cc.Nodes = nodes
@@ -1477,6 +1509,7 @@ func createNode(cc config.ClusterConfig, kubeNodeName string, existing *config.C
14771509
cp := config.Node{
14781510
Port: cc.KubernetesConfig.NodePort,
14791511
KubernetesVersion: getKubernetesVersion(&cc),
1512+
ContainerRuntime: getContainerRuntime(&cc),
14801513
Name: kubeNodeName,
14811514
ControlPlane: true,
14821515
Worker: true,
@@ -1573,6 +1606,17 @@ func validateKubernetesVersion(old *config.ClusterConfig) {
15731606
}
15741607
}
15751608

1609+
// validateContainerRuntime ensures that the container runtime is reasonable
1610+
func validateContainerRuntime(old *config.ClusterConfig) {
1611+
if old == nil || old.KubernetesConfig.ContainerRuntime == "" {
1612+
return
1613+
}
1614+
1615+
if err := validateRuntime(old.KubernetesConfig.ContainerRuntime); err != nil {
1616+
klog.Errorf("Error parsing old runtime %q: %v", old.KubernetesConfig.ContainerRuntime, err)
1617+
}
1618+
}
1619+
15761620
func isBaseImageApplicable(drv string) bool {
15771621
return registry.IsKIC(drv)
15781622
}

cmd/minikube/cmd/start_flags.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func initMinikubeFlags() {
158158
startCmd.Flags().String(kicBaseImage, kic.BaseImage, "The base image to use for docker/podman drivers. Intended for local development.")
159159
startCmd.Flags().Bool(keepContext, false, "This will keep the existing kubectl context and will create a minikube context.")
160160
startCmd.Flags().Bool(embedCerts, false, "if true, will embed the certs in kubeconfig.")
161-
startCmd.Flags().String(containerRuntime, constants.DefaultContainerRuntime, fmt.Sprintf("The container runtime to be used (%s).", strings.Join(cruntime.ValidRuntimes(), ", ")))
161+
startCmd.Flags().String(containerRuntime, constants.DefaultContainerRuntime, fmt.Sprintf("The container runtime to be used. Valid options: %s (default: auto)", strings.Join(cruntime.ValidRuntimes(), ", ")))
162162
startCmd.Flags().Bool(createMount, false, "This will start the mount daemon and automatically mount files into minikube.")
163163
startCmd.Flags().String(mountString, constants.DefaultMountDir+":/minikube-host", "The argument to pass the minikube mount command on start.")
164164
startCmd.Flags().String(mount9PVersion, defaultMount9PVersion, mount9PVersionDescription)
@@ -270,7 +270,7 @@ func ClusterFlagValue() string {
270270
}
271271

272272
// generateClusterConfig generate a config.ClusterConfig based on flags or existing cluster config
273-
func generateClusterConfig(cmd *cobra.Command, existing *config.ClusterConfig, k8sVersion string, drvName string) (config.ClusterConfig, config.Node, error) {
273+
func generateClusterConfig(cmd *cobra.Command, existing *config.ClusterConfig, k8sVersion string, rtime string, drvName string) (config.ClusterConfig, config.Node, error) {
274274
var cc config.ClusterConfig
275275
if existing != nil {
276276
cc = updateExistingConfigFromFlags(cmd, existing)
@@ -282,7 +282,7 @@ func generateClusterConfig(cmd *cobra.Command, existing *config.ClusterConfig, k
282282
}
283283
} else {
284284
klog.Info("no existing cluster config was found, will generate one from the flags ")
285-
cc = generateNewConfigFromFlags(cmd, k8sVersion, drvName)
285+
cc = generateNewConfigFromFlags(cmd, k8sVersion, rtime, drvName)
286286

287287
cnm, err := cni.New(&cc)
288288
if err != nil {
@@ -419,7 +419,7 @@ func getCNIConfig(cmd *cobra.Command) string {
419419
}
420420

421421
// generateNewConfigFromFlags generate a config.ClusterConfig based on flags
422-
func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, drvName string) config.ClusterConfig {
422+
func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, rtime string, drvName string) config.ClusterConfig {
423423
var cc config.ClusterConfig
424424

425425
// networkPlugin cni deprecation warning
@@ -499,7 +499,7 @@ func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, drvName s
499499
APIServerIPs: apiServerIPs,
500500
DNSDomain: viper.GetString(dnsDomain),
501501
FeatureGates: viper.GetString(featureGates),
502-
ContainerRuntime: viper.GetString(containerRuntime),
502+
ContainerRuntime: rtime,
503503
CRISocket: viper.GetString(criSocket),
504504
NetworkPlugin: chosenNetworkPlugin,
505505
ServiceCIDR: viper.GetString(serviceCIDR),
@@ -533,7 +533,7 @@ func generateNewConfigFromFlags(cmd *cobra.Command, k8sVersion string, drvName s
533533
exit.Message(reason.Usage, "Ensure your {{.driver_name}} is running and is healthy.", out.V{"driver_name": driver.FullName(drvName)})
534534
}
535535
if si.Rootless {
536-
if cc.KubernetesConfig.ContainerRuntime == "docker" {
536+
if cc.KubernetesConfig.ContainerRuntime == constants.Docker {
537537
exit.Message(reason.Usage, "--container-runtime must be set to \"containerd\" or \"cri-o\" for rootless")
538538
}
539539
// KubeletInUserNamespace feature gate is essential for rootless driver.
@@ -711,6 +711,9 @@ func updateExistingConfigFromFlags(cmd *cobra.Command, existing *config.ClusterC
711711
if cmd.Flags().Changed(kubernetesVersion) {
712712
cc.KubernetesConfig.KubernetesVersion = getKubernetesVersion(existing)
713713
}
714+
if cmd.Flags().Changed(containerRuntime) {
715+
cc.KubernetesConfig.ContainerRuntime = getContainerRuntime(existing)
716+
}
714717

715718
if cmd.Flags().Changed("extra-config") {
716719
cc.KubernetesConfig.ExtraOptions = config.ExtraOptions

cmd/minikube/cmd/start_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func TestMirrorCountry(t *testing.T) {
112112
viper.SetDefault(humanReadableDiskSize, defaultDiskSize)
113113
checkRepository = checkRepoMock
114114
k8sVersion := constants.DefaultKubernetesVersion
115+
rtime := constants.DefaultContainerRuntime
115116
var tests = []struct {
116117
description string
117118
k8sVersion string
@@ -157,7 +158,7 @@ func TestMirrorCountry(t *testing.T) {
157158
viper.SetDefault(imageRepository, test.imageRepository)
158159
viper.SetDefault(imageMirrorCountry, test.mirrorCountry)
159160
viper.SetDefault(kvmNUMACount, 1)
160-
config, _, err := generateClusterConfig(cmd, nil, k8sVersion, driver.Mock)
161+
config, _, err := generateClusterConfig(cmd, nil, k8sVersion, rtime, driver.Mock)
161162
if err != nil {
162163
t.Fatalf("Got unexpected error %v during config generation", err)
163164
}
@@ -179,6 +180,7 @@ func TestGenerateCfgFromFlagsHTTPProxyHandling(t *testing.T) {
179180
}
180181
}()
181182
k8sVersion := constants.NewestKubernetesVersion
183+
rtime := constants.DefaultContainerRuntime
182184
var tests = []struct {
183185
description string
184186
proxy string
@@ -226,7 +228,7 @@ func TestGenerateCfgFromFlagsHTTPProxyHandling(t *testing.T) {
226228

227229
cfg.DockerEnv = []string{} // clear docker env to avoid pollution
228230
proxy.SetDockerEnv()
229-
config, _, err := generateClusterConfig(cmd, nil, k8sVersion, "none")
231+
config, _, err := generateClusterConfig(cmd, nil, k8sVersion, rtime, "none")
230232
if err != nil {
231233
t.Fatalf("Got unexpected error %v during config generation", err)
232234
}

pkg/minikube/cni/cni.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func chooseDefault(cc config.ClusterConfig) Manager {
155155
return KindNet{cc: cc}
156156
}
157157

158-
if cc.KubernetesConfig.ContainerRuntime != "docker" {
158+
if cc.KubernetesConfig.ContainerRuntime != constants.Docker {
159159
if driver.IsKIC(cc.Driver) {
160160
klog.Infof("%q driver + %s runtime found, recommending kindnet", cc.Driver, cc.KubernetesConfig.ContainerRuntime)
161161
return KindNet{cc: cc}

pkg/minikube/cni/disabled.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cni
1919
import (
2020
"k8s.io/klog/v2"
2121
"k8s.io/minikube/pkg/minikube/config"
22+
"k8s.io/minikube/pkg/minikube/constants"
2223
"k8s.io/minikube/pkg/minikube/driver"
2324
)
2425

@@ -34,7 +35,7 @@ func (c Disabled) String() string {
3435

3536
// Apply enables the CNI
3637
func (c Disabled) Apply(r Runner) error {
37-
if driver.IsKIC(c.cc.Driver) && c.cc.KubernetesConfig.ContainerRuntime != "docker" {
38+
if driver.IsKIC(c.cc.Driver) && c.cc.KubernetesConfig.ContainerRuntime != constants.Docker {
3839
klog.Warningf("CNI is recommended for %q driver and %q runtime - expect networking issues", c.cc.Driver, c.cc.KubernetesConfig.ContainerRuntime)
3940
}
4041

pkg/minikube/config/profile.go

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func PrimaryControlPlane(cc *ClusterConfig) (Node, error) {
6363
IP: cc.KubernetesConfig.NodeIP,
6464
Port: cc.KubernetesConfig.NodePort,
6565
KubernetesVersion: cc.KubernetesConfig.KubernetesVersion,
66+
ContainerRuntime: cc.KubernetesConfig.ContainerRuntime,
6667
ControlPlane: true,
6768
Worker: true,
6869
}

pkg/minikube/config/types.go

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ type Node struct {
134134
IP string
135135
Port int
136136
KubernetesVersion string
137+
ContainerRuntime string
137138
ControlPlane bool
138139
Worker bool
139140
}

pkg/minikube/constants/constants.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ const (
6060
Containerd = "containerd"
6161
// CRIO is the default name and spelling for the cri-o container runtime
6262
CRIO = "crio"
63+
// Docker is the default name and spelling for the docker container runtime
64+
Docker = "docker"
6365
// DefaultContainerRuntime is our default container runtime
64-
DefaultContainerRuntime = "docker"
66+
DefaultContainerRuntime = ""
6567

6668
// APIServerName is the default API server name
6769
APIServerName = "minikubeCA"

pkg/minikube/download/download_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func testPreloadDownloadPreventsMultipleDownload(t *testing.T) {
9494
var group sync.WaitGroup
9595
group.Add(2)
9696
dlCall := func() {
97-
if err := Preload(constants.DefaultKubernetesVersion, constants.DefaultContainerRuntime, "docker"); err != nil {
97+
if err := Preload(constants.DefaultKubernetesVersion, constants.Docker, "docker"); err != nil {
9898
t.Logf("Failed to download preload: %+v (may be ok)", err)
9999
}
100100
group.Done()
@@ -119,7 +119,7 @@ func testPreloadNotExists(t *testing.T) {
119119
getChecksum = func(k8sVersion, containerRuntime string) ([]byte, error) { return []byte("check"), nil }
120120
ensureChecksumValid = func(k8sVersion, containerRuntime, path string, checksum []byte) error { return nil }
121121

122-
err := Preload(constants.DefaultKubernetesVersion, constants.DefaultContainerRuntime, "docker")
122+
err := Preload(constants.DefaultKubernetesVersion, constants.Docker, "docker")
123123
if err != nil {
124124
t.Errorf("Expected no error when preload exists")
125125
}
@@ -140,7 +140,7 @@ func testPreloadChecksumMismatch(t *testing.T) {
140140
return fmt.Errorf("checksum mismatch")
141141
}
142142

143-
err := Preload(constants.DefaultKubernetesVersion, constants.DefaultContainerRuntime, "docker")
143+
err := Preload(constants.DefaultKubernetesVersion, constants.Docker, "docker")
144144
expectedErrMsg := "checksum mismatch"
145145
if err == nil {
146146
t.Errorf("Expected error when checksum mismatches")

site/content/en/docs/commands/start.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ minikube start [flags]
3030
--cache-images If true, cache docker images for the current bootstrapper and load them into the machine. Always false with --driver=none. (default true)
3131
--cert-expiration duration Duration until minikube certificate expiration, defaults to three years (26280h). (default 26280h0m0s)
3232
--cni string CNI plug-in to use. Valid options: auto, bridge, calico, cilium, flannel, kindnet, or path to a CNI manifest (default: auto)
33-
--container-runtime string The container runtime to be used (docker, cri-o, containerd). (default "docker")
33+
--container-runtime string The container runtime to be used. Valid options: docker, cri-o, containerd (default: auto)
3434
--cpus string Number of CPUs allocated to Kubernetes. Use "max" to use the maximum number of CPUs. (default "2")
3535
--cri-socket string The cri socket path to be used.
3636
--delete-on-failure If set, delete the current cluster if start fails and try again. Defaults to false.

site/content/en/docs/handbook/config.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ minikube start --extra-config=kubeadm.ignore-preflight-errors=SystemVerification
8989

9090
## Runtime configuration
9191

92-
The default container runtime in minikube is Docker. You can select it explicitly by using:
92+
The default container runtime in minikube varies. You can select one explicitly by using:
9393

9494
```shell
9595
minikube start --container-runtime=docker
@@ -100,6 +100,8 @@ Other options available are:
100100
* [containerd](https://github.com/containerd/containerd)
101101
* [cri-o](https://github.com/cri-o/cri-o)
102102

103+
See <https://kubernetes.io/docs/setup/production-environment/container-runtimes/>
104+
103105
## Environment variables
104106

105107
minikube supports passing environment variables instead of flags for every value listed in `minikube config`. This is done by passing an environment variable with the prefix `MINIKUBE_`.

translations/strings.txt

+1
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@
647647
"The path on the file system where the testing docs in markdown need to be saved": "",
648648
"The podman service within '{{.cluster}}' is not active": "",
649649
"The podman-env command is incompatible with multi-node clusters. Use the 'registry' add-on: https://minikube.sigs.k8s.io/docs/handbook/registry/": "",
650+
"The podman-env command is only compatible with the \"crio\" runtime, but this cluster was configured to use the \"{{.runtime}}\" runtime.": "",
650651
"The requested memory allocation of {{.requested}}MiB does not leave room for system overhead (total system memory: {{.system_limit}}MiB). You may face stability issues.": "",
651652
"The service namespace": "",
652653
"The service/ingress {{.resource}} requires privileged ports to be exposed: {{.ports}}": "",

0 commit comments

Comments
 (0)