diff --git a/DOCS.md b/DOCS.md index 22f718f..434ceff 100644 --- a/DOCS.md +++ b/DOCS.md @@ -21,6 +21,7 @@ The following parameters are used to configure this plugin: * `vars` - variables to use in `template` and `secret_template` (see [below](#available-vars) for details) * `secrets` - credential and variables to use in `secret_template` (see [below](#secrets) for details) * *optional* `expand_env_vars` - expand environment variables for values in `vars` for reference (defaults to `false`) +* *optional* `kubectl_version` - version of kubectl executable to use (see [below](#using-extra-kubectl-versions) for details) ### Debugging parameters @@ -114,6 +115,43 @@ To use `$${IMAGE_VERSION}` or `$IMAGE_VERSION`, see the [Drone docs][environment [expand]: https://golang.org/pkg/os/#ExpandEnv [environment]: http://docs.drone.io/environment/ +## Using "extra" `kubectl` versions + +### tl;dr + +To run `drone-gke` using a different version of `kubectl` than the default, set `kubectl-version` to the version you'd like to use. + +For example, to use the **1.14** version of `kubectl`: + +```yml +image: nytimes/drone-gke:0.9.1 +kubectl_version: "1.14" +``` + +This will configure the plugin to execute `/google-cloud-sdk/bin/kubectl.1.14` instead of `/google-cloud-sdk/bin/kubectl` for all `kubectl` commands. + +### Background + +Beginning with the [`237.0.0 (2019-03-05)` release of the `gcloud` SDK](https://cloud.google.com/sdk/docs/release-notes#23700_2019-03-05), "extra" `kubectl` versions are installed automatically when `kubectl` is installed via `gcloud components install kubectl`. + +These "extra" versions are installed alongside the SDK's default `kubectl` version at `/google-cloud-sdk/bin` and are named using the following pattern: + +```sh +kubectl.$clientVersionMajor.$clientVersionMinor +``` + +To list all of the "extra" `kubectl` versions available within a particular version of `drone-gke`, you can run the following: + +```sh +# list "extra" kubectl versions available with nytimes/drone-gke:0.9.1 +docker run \ + --rm \ + --interactive \ + --tty \ + --entrypoint '' \ + nytimes/drone-gke:0.9.1 list-extra-kubectl-versions +``` + ## Example reference usage ### `.drone.yml` diff --git a/Dockerfile b/Dockerfile index a422086..d2d0cd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,5 @@ ADD bin/install-kubectl /usr/local/bin/ RUN install-kubectl && rm -f /usr/local/bin/install-kubectl FROM cloud-sdk -ADD drone-gke /usr/local/bin/ -ENTRYPOINT ["drone-gke"] +ADD drone-gke bin/entrypoint bin/list-extra-kubectl-versions /usr/local/bin/ +ENTRYPOINT ["entrypoint", "drone-gke"] diff --git a/bin/entrypoint b/bin/entrypoint new file mode 100755 index 0000000..e977242 --- /dev/null +++ b/bin/entrypoint @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e + +export EXTRA_KUBECTL_VERSIONS=$(echo $(list-extra-kubectl-versions)) + +exec "$@" \ No newline at end of file diff --git a/bin/list-extra-kubectl-versions b/bin/list-extra-kubectl-versions new file mode 100755 index 0000000..33bb46a --- /dev/null +++ b/bin/list-extra-kubectl-versions @@ -0,0 +1,14 @@ +#!/usr/bin/env sh +set -e + +# location to search for extra kubectl version +# defaults to default used by `gcloud components install kubectl` +gcloud_sdk_bin_dir=${1:-'/google-cloud-sdk/bin'} + +# filename glob for extra kubectl versions +# defaults to default used by `gcloud components install kubectl` +extra_kubectl_glob=${2:-'kubectl.*'} + +find ${gcloud_sdk_bin_dir} -type f -name ${extra_kubectl_glob} -exec basename {} \; \ + | cut -d '.' -f 2,3 \ + | sort -g \ No newline at end of file diff --git a/main.go b/main.go index f2d644a..fdc8827 100644 --- a/main.go +++ b/main.go @@ -31,15 +31,18 @@ var ( ) const ( - gcloudCmd = "gcloud" - kubectlCmd = "kubectl" - timeoutCmd = "timeout" + gcloudCmd = "gcloud" + kubectlCmdName = "kubectl" + timeoutCmd = "timeout" keyPath = "/tmp/gcloud.json" nsPath = "/tmp/namespace.json" templateBasePath = "/tmp" ) +// default to kubectlCmdName, can be overriden via kubectl-version param +var kubectlCmd = kubectlCmdName +var extraKubectlVersions = strings.Split(os.Getenv("EXTRA_KUBECTL_VERSIONS"), " ") var nsTemplate = ` --- apiVersion: v1 @@ -167,6 +170,12 @@ func wrapMain() error { EnvVar: "PLUGIN_WAIT_SECONDS", Value: 0, }, + cli.StringFlag{ + Name: "kubectl-version", + Usage: "optional - version of kubectl binary to use, e.g. 1.14", + EnvVar: "PLUGIN_KUBECTL_VERSION", + Value: "", + }, } if err := app.Run(os.Args); err != nil { @@ -192,6 +201,12 @@ func run(c *cli.Context) error { } } + // Use custom kubectl version if provided + kubectlVersion := c.String("kubectl-version") + if kubectlVersion != "" { + kubectlCmd = fmt.Sprintf("%s.%s", kubectlCmdName, kubectlVersion) + } + // Parse variables and secrets vars, err := parseVars(c) if err != nil { @@ -293,9 +308,53 @@ func checkParams(c *cli.Context) error { return fmt.Errorf("Missing required param: cluster") } + if !isValidKubectlVersionParam(c, extraKubectlVersions) { + return fmt.Errorf(getInvalidKubectlVersionMessage(c, extraKubectlVersions)) + } + return nil } +// isValidKubectlVersionParam tests whether a given version is valid within the current environment +func isValidKubectlVersionParam(c *cli.Context, availableVersions []string) bool { + kubectlVersionParam := c.String("kubectl-version") + // using the default version + if kubectlVersionParam == "" { + return true + } + + // using a custom version but no extra versions are available + if len(availableVersions) == 0 { + return false + } + + // using a custom version ... + // return true if included in available extra versions; false otherwise + for _, availableVersion := range availableVersions { + if kubectlVersionParam == availableVersion { + return true + } + } + return false +} + +// getInvalidKubectlVersionMessage returns an error message for invalid kubectl-version values +func getInvalidKubectlVersionMessage(c *cli.Context, availableVersions []string) string { + kubectlVersionParam := c.String("kubectl-version") + // kubectl-version is valid; return an empty string + if isValidKubectlVersionParam(c, availableVersions) { + return "" + } + + // using a custom version but no extra versions are available + if len(availableVersions) == 0 { + return fmt.Sprintf("Invalid param: kubectl-version was set to %s but no extra kubectl versions are available", kubectlVersionParam) + } + + // using a custom version and extra versions are available, but specified version is not included + return fmt.Sprintf("Invalid param kubectl-version: %s must be one of %s", kubectlVersionParam, strings.Join(availableVersions, ", ")) +} + // getProjectFromToken gets project id from token func getProjectFromToken(j string) string { t := token{} @@ -529,7 +588,7 @@ func setNamespace(c *cli.Context, project string, runner Runner) error { //replace invalid char in namespace namespace = strings.ToLower(namespace) namespace = invalidNameRegex.ReplaceAllString(namespace, "-") - + // Set the execution namespace. log("Configuring kubectl to the %s namespace\n", namespace) diff --git a/main_test.go b/main_test.go index 4e9cffc..e0c469b 100644 --- a/main_test.go +++ b/main_test.go @@ -74,6 +74,76 @@ func TestCheckParams(t *testing.T) { assert.NoError(t, err) } +func TestIsValidKubectlVersionParam(t *testing.T) { + // kubectl-version is NOT set (using default kubectl version) + set := flag.NewFlagSet("default-kubectl-version", 0) + c := cli.NewContext(nil, set, nil) + availableVersions := []string{} + isValid := isValidKubectlVersionParam(c, availableVersions) + assert.True(t, isValid, "expected isValidKubectlVersionParam to be true when no kubectl-version param was set") + + // kubectl-version is set and extra kubectl versions are NOT available + set = flag.NewFlagSet("kubectl-version-set-no-extra-versions-available", 0) + c = cli.NewContext(nil, set, nil) + set.String("kubectl-version", "1.14", "") + availableVersions = []string{} + isValid = isValidKubectlVersionParam(c, availableVersions) + assert.False(t, isValid, "expected isValidKubectlVersionParam to be false when no extra kubectl versions are available") + + // kubectl-version is set, extra kubectl versions are available, kubectl-version is included + set = flag.NewFlagSet("valid-kubectl-version", 0) + c = cli.NewContext(nil, set, nil) + set.String("kubectl-version", "1.13", "") + availableVersions = []string{"1.11", "1.12", "1.13", "1.14"} + isValid = isValidKubectlVersionParam(c, availableVersions) + assert.True(t, isValid, "expected isValidKubectlVersionParam to be true when kubectl-version is set, extra kubectl versions are available, kubectl-version is included") + + // kubectl-version is set, extra kubectl versions are available, kubectl-version is NOT included + set = flag.NewFlagSet("invalid-kubectl-version", 0) + c = cli.NewContext(nil, set, nil) + set.String("kubectl-version", "9.99", "") + availableVersions = []string{"1.11", "1.12", "1.13", "1.14"} + isValid = isValidKubectlVersionParam(c, availableVersions) + assert.False(t, isValid, "expected isValidKubectlVersionParam to be false when kubectl-version is set, extra kubectl versions are available, kubectl-version is NOT included") +} + +func TestGetInvalidKubectlVersionMessage(t *testing.T) { + // kubectl-version is NOT set (using default kubectl version) + set := flag.NewFlagSet("default-kubectl-version", 0) + c := cli.NewContext(nil, set, nil) + availableVersions := []string{} + expected := "" + actual := getInvalidKubectlVersionMessage(c, availableVersions) + assert.Equal(t, expected, actual, "expected getInvalidKubectlVersionMessage to return empty string when kubectl-version is NOT set") + + // kubectl-version is set and extra kubectl versions are NOT available + set = flag.NewFlagSet("kubectl-version-set-no-extra-versions-available", 0) + c = cli.NewContext(nil, set, nil) + set.String("kubectl-version", "1.14", "") + availableVersions = []string{} + expected = "Invalid param: kubectl-version was set to 1.14 but no extra kubectl versions are available" + actual = getInvalidKubectlVersionMessage(c, availableVersions) + assert.Equal(t, expected, actual, "expected getInvalidKubectlVersionMessage to return matching string when kubectl-version is set and extra kubectl versions are NOT available") + + // kubectl-version is set, extra kubectl versions are available, kubectl-version is included + set = flag.NewFlagSet("valid-kubectl-version", 0) + c = cli.NewContext(nil, set, nil) + set.String("kubectl-version", "1.13", "") + availableVersions = []string{"1.11", "1.12", "1.13", "1.14"} + expected = "" + actual = getInvalidKubectlVersionMessage(c, availableVersions) + assert.Equal(t, expected, actual, "expected getInvalidKubectlVersionMessage to return empty string when kubectl-version is set, extra kubectl versions are available, kubectl-version is included") + + // kubectl-version is set, extra kubectl versions are available, kubectl-version is NOT included + set = flag.NewFlagSet("invalid-kubectl-version", 0) + c = cli.NewContext(nil, set, nil) + set.String("kubectl-version", "9.99", "") + availableVersions = []string{"1.11", "1.12", "1.13", "1.14"} + expected = "Invalid param kubectl-version: 9.99 must be one of 1.11, 1.12, 1.13, 1.14" + actual = getInvalidKubectlVersionMessage(c, availableVersions) + assert.Equal(t, expected, actual, "expected getInvalidKubectlVersionMessage to return matching string when kubectl-version is set, extra kubectl versions are available, kubectl-version is NOT included") +} + func TestGetProjectFromToken(t *testing.T) { token := "{\"project_id\":\"test-project\"}" assert.Equal(t, "test-project", getProjectFromToken(token))