From 6cb903ecd61d30ccdfa2f4e98bcc121e57d6ebd2 Mon Sep 17 00:00:00 2001 From: Nick Santos Date: Fri, 4 Dec 2020 01:10:48 -0500 Subject: [PATCH] cluster: add the ability to load a specific kubernetes version on kind. Fixes https://github.com/tilt-dev/ctlptl/issues/66 (#67) --- go.mod | 1 + go.sum | 4 + pkg/cluster/admin_kind.go | 92 ++++++++++++++++++++++ pkg/cluster/admin_kind_test.go | 32 ++++++++ pkg/cluster/cluster.go | 31 +++++++- test/kind-cluster-network/cluster.yaml | 2 +- test/minikube-cluster-network/cluster.yaml | 1 + 7 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 pkg/cluster/admin_kind_test.go diff --git a/go.mod b/go.mod index 5927b87..42e6f40 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( cloud.google.com/go v0.56.0 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/blang/semver/v4 v4.0.0 github.com/containerd/containerd v1.4.1 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v17.12.0-ce-rc1.0.20200730172259-9f28837c1d93+incompatible diff --git a/go.sum b/go.sum index 9118f92..1c6ed90 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v1.1.0 h1:ol1rO7QQB5uy7umSNV7VAmLugfLRD+17sYJujRNYPhg= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= diff --git a/pkg/cluster/admin_kind.go b/pkg/cluster/admin_kind.go index d63dea8..97a961d 100644 --- a/pkg/cluster/admin_kind.go +++ b/pkg/cluster/admin_kind.go @@ -6,6 +6,7 @@ import ( "os/exec" "strings" + "github.com/blang/semver/v4" "github.com/pkg/errors" "github.com/tilt-dev/ctlptl/pkg/api" "github.com/tilt-dev/localregistry-go" @@ -49,6 +50,18 @@ func (a *kindAdmin) Create(ctx context.Context, desired *api.Cluster, registry * kindName := strings.TrimPrefix(clusterName, "kind-") args := []string{"create", "cluster", "--name", kindName} + if desired.KubernetesVersion != "" { + kindVersion, err := a.getKindVersion(ctx) + if err != nil { + return errors.Wrap(err, "creating cluster") + } + + node, err := a.getNodeImage(ctx, kindVersion, desired.KubernetesVersion) + if err != nil { + return errors.Wrap(err, "creating cluster") + } + args = append(args, "--image", node) + } // TODO(nick): Let the user pass in their own Kind configuration. in := strings.NewReader("") @@ -125,3 +138,82 @@ func (a *kindAdmin) Delete(ctx context.Context, config *api.Cluster) error { } return nil } + +func (a *kindAdmin) getNodeImage(ctx context.Context, kindVersion, k8sVersion string) (string, error) { + nodeTable, ok := kindK8sNodeTable[kindVersion] + if !ok { + return "", fmt.Errorf("No available kindest/node versions for kind version %s.\n"+ + "Please file an issue: https://github.com/tilt-dev/ctlptl/issues/new", kindVersion) + } + + // Kind doesn't maintain Kubernetes nodes for every patch version, so just get the closest + // major/minor patch. + k8sVersionParsed, err := semver.ParseTolerant(k8sVersion) + if err != nil { + return "", fmt.Errorf("parsing kubernetesVersion: %v", err) + } + + simplifiedK8sVersion := fmt.Sprintf("%d.%d", k8sVersionParsed.Major, k8sVersionParsed.Minor) + node, ok := nodeTable[simplifiedK8sVersion] + if !ok { + return "", fmt.Errorf("Kind %s does not support Kubernetes v%s", kindVersion, simplifiedK8sVersion) + } + return node, nil +} + +func (a *kindAdmin) getKindVersion(ctx context.Context) (string, error) { + cmd := exec.CommandContext(ctx, "kind", "version") + out, err := cmd.Output() + if err != nil { + return "", errors.Wrap(err, "kind version") + } + + parts := strings.Split(string(out), " ") + if len(parts) < 2 { + return "", fmt.Errorf("parsing kind version output: %s", string(out)) + } + + return parts[1], nil +} + +// This table must be built up manually from the Kind release notes each +// time a new Kind version is released :\ +var kindK8sNodeTable = map[string]map[string]string{ + "v0.9.0": map[string]string{ + "1.19": "kindest/node:v1.19.1@sha256:98cf5288864662e37115e362b23e4369c8c4a408f99cbc06e58ac30ddc721600", + "1.18": "kindest/node:v1.18.8@sha256:f4bcc97a0ad6e7abaf3f643d890add7efe6ee4ab90baeb374b4f41a4c95567eb", + "1.17": "kindest/node:v1.17.11@sha256:5240a7a2c34bf241afb54ac05669f8a46661912eab05705d660971eeb12f6555", + "1.16": "kindest/node:v1.16.15@sha256:a89c771f7de234e6547d43695c7ab047809ffc71a0c3b65aa54eda051c45ed20", + "1.15": "kindest/node:v1.15.12@sha256:d9b939055c1e852fe3d86955ee24976cab46cba518abcb8b13ba70917e6547a6", + "1.14": "kindest/node:v1.14.10@sha256:ce4355398a704fca68006f8a29f37aafb49f8fc2f64ede3ccd0d9198da910146", + "1.13": "kindest/node:v1.13.12@sha256:1c1a48c2bfcbae4d5f4fa4310b5ed10756facad0b7a2ca93c7a4b5bae5db29f5", + }, + "v0.8.1": map[string]string{ + "1.18": "kindest/node:v1.18.2@sha256:7b27a6d0f2517ff88ba444025beae41491b016bc6af573ba467b70c5e8e0d85f", + "1.17": "kindest/node:v1.17.5@sha256:ab3f9e6ec5ad8840eeb1f76c89bb7948c77bbf76bcebe1a8b59790b8ae9a283a", + "1.16": "kindest/node:v1.16.9@sha256:7175872357bc85847ec4b1aba46ed1d12fa054c83ac7a8a11f5c268957fd5765", + "1.15": "kindest/node:v1.15.11@sha256:6cc31f3533deb138792db2c7d1ffc36f7456a06f1db5556ad3b6927641016f50", + "1.14": "kindest/node:v1.14.10@sha256:6cd43ff41ae9f02bb46c8f455d5323819aec858b99534a290517ebc181b443c6", + "1.13": "kindest/node:v1.13.12@sha256:214476f1514e47fe3f6f54d0f9e24cfb1e4cda449529791286c7161b7f9c08e7", + "1.12": "kindest/node:v1.12.10@sha256:faeb82453af2f9373447bb63f50bae02b8020968e0889c7fa308e19b348916cb", + }, + "v0.8.0": map[string]string{ + "1.18": "kindest/node:v1.18.2@sha256:7b27a6d0f2517ff88ba444025beae41491b016bc6af573ba467b70c5e8e0d85f", + "1.17": "kindest/node:v1.17.5@sha256:ab3f9e6ec5ad8840eeb1f76c89bb7948c77bbf76bcebe1a8b59790b8ae9a283a", + "1.16": "kindest/node:v1.16.9@sha256:7175872357bc85847ec4b1aba46ed1d12fa054c83ac7a8a11f5c268957fd5765", + "1.15": "kindest/node:v1.15.11@sha256:6cc31f3533deb138792db2c7d1ffc36f7456a06f1db5556ad3b6927641016f50", + "1.14": "kindest/node:v1.14.10@sha256:6cd43ff41ae9f02bb46c8f455d5323819aec858b99534a290517ebc181b443c6", + "1.13": "kindest/node:v1.13.12@sha256:214476f1514e47fe3f6f54d0f9e24cfb1e4cda449529791286c7161b7f9c08e7", + "1.12": "kindest/node:v1.12.10@sha256:faeb82453af2f9373447bb63f50bae02b8020968e0889c7fa308e19b348916cb", + }, + "v0.7.0": map[string]string{ + "1.18": "kindest/node:v1.18.0@sha256:0e20578828edd939d25eb98496a685c76c98d54084932f76069f886ec315d694", + "1.17": "kindest/node:v1.17.0@sha256:9512edae126da271b66b990b6fff768fbb7cd786c7d39e86bdf55906352fdf62", + "1.16": "kindest/node:v1.16.4@sha256:b91a2c2317a000f3a783489dfb755064177dbc3a0b2f4147d50f04825d016f55", + "1.15": "kindest/node:v1.15.7@sha256:e2df133f80ef633c53c0200114fce2ed5e1f6947477dbc83261a6a921169488d", + "1.14": "kindest/node:v1.14.10@sha256:81ae5a3237c779efc4dda43cc81c696f88a194abcc4f8fa34f86cf674aa14977", + "1.13": "kindest/node:v1.13.12@sha256:5e8ae1a4e39f3d151d420ef912e18368745a2ede6d20ea87506920cd947a7e3a", + "1.12": "kindest/node:v1.12.10@sha256:68a6581f64b54994b824708286fafc37f1227b7b54cbb8865182ce1e036ed1cc", + "1.11": "kindest/node:v1.11.10@sha256:e6f3dade95b7cb74081c5b9f3291aaaa6026a90a977e0b990778b6adc9ea6248", + }, +} diff --git a/pkg/cluster/admin_kind_test.go b/pkg/cluster/admin_kind_test.go new file mode 100644 index 0000000..016382f --- /dev/null +++ b/pkg/cluster/admin_kind_test.go @@ -0,0 +1,32 @@ +package cluster + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func TestNodeImage(t *testing.T) { + iostreams := genericclioptions.IOStreams{ + In: os.Stdin, + Out: os.Stdout, + ErrOut: os.Stderr, + } + a := newKindAdmin(iostreams) + ctx := context.Background() + + img, err := a.getNodeImage(ctx, "v0.9.0", "v1.19") + assert.NoError(t, err) + assert.Equal(t, "kindest/node:v1.19.1@sha256:98cf5288864662e37115e362b23e4369c8c4a408f99cbc06e58ac30ddc721600", img) + + img, err = a.getNodeImage(ctx, "v0.9.0", "v1.19.3") + assert.NoError(t, err) + assert.Equal(t, "kindest/node:v1.19.1@sha256:98cf5288864662e37115e362b23e4369c8c4a408f99cbc06e58ac30ddc721600", img) + + img, err = a.getNodeImage(ctx, "v0.8.1", "v1.16.1") + assert.NoError(t, err) + assert.Equal(t, "kindest/node:v1.16.9@sha256:7175872357bc85847ec4b1aba46ed1d12fa054c83ac7a8a11f5c268957fd5765", img) +} diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 98a2156..cc23513 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -6,6 +6,7 @@ import ( "sort" "sync" + "github.com/blang/semver/v4" "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/tilt-dev/ctlptl/pkg/api" @@ -381,7 +382,32 @@ func supportsRegistry(product Product) bool { } func supportsKubernetesVersion(product Product, version string) bool { - return product == ProductMinikube + return product == ProductKIND || product == ProductMinikube +} + +func (c *Controller) canReconcileK8sVersion(ctx context.Context, desired, existing *api.Cluster) bool { + if desired.KubernetesVersion == "" { + return true + } + + if desired.KubernetesVersion == existing.Status.KubernetesVersion { + return true + } + + // On KIND, it's ok if the patch doesn't match. + if Product(desired.Product) == ProductKIND { + dv, err := semver.ParseTolerant(desired.KubernetesVersion) + if err != nil { + return false + } + ev, err := semver.ParseTolerant(existing.Status.KubernetesVersion) + if err != nil { + return false + } + return dv.Major == ev.Major && dv.Minor == ev.Minor + } + + return false } func (c *Controller) deleteIfIrreconcilable(ctx context.Context, desired, existing *api.Cluster) error { @@ -401,8 +427,7 @@ func (c *Controller) deleteIfIrreconcilable(ctx context.Context, desired, existi _, _ = fmt.Fprintf(c.iostreams.ErrOut, "Deleting cluster %s to initialize with registry %s\n", desired.Name, desired.Registry) needsDelete = true - } else if desired.KubernetesVersion != "" && - desired.KubernetesVersion != existing.Status.KubernetesVersion { + } else if !c.canReconcileK8sVersion(ctx, desired, existing) { _, _ = fmt.Fprintf(c.iostreams.ErrOut, "Deleting cluster %s because desired Kubernetes version (%s) does not match current (%s)\n", desired.Name, desired.KubernetesVersion, existing.Status.KubernetesVersion) diff --git a/test/kind-cluster-network/cluster.yaml b/test/kind-cluster-network/cluster.yaml index 4f7773a..9bccde1 100644 --- a/test/kind-cluster-network/cluster.yaml +++ b/test/kind-cluster-network/cluster.yaml @@ -3,4 +3,4 @@ kind: Cluster name: kind-ctlptl-test-cluster product: kind registry: ctlptl-test-registry - +kubernetesVersion: v1.19.3 diff --git a/test/minikube-cluster-network/cluster.yaml b/test/minikube-cluster-network/cluster.yaml index ba0274f..ee3bc86 100644 --- a/test/minikube-cluster-network/cluster.yaml +++ b/test/minikube-cluster-network/cluster.yaml @@ -3,4 +3,5 @@ kind: Cluster name: minikube-ctlptl-test-cluster product: minikube registry: ctlptl-test-registry +kubernetesVersion: v1.19.3