1/*
2Copyright 2018 The Doctl Authors All rights reserved.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6	http://www.apache.org/licenses/LICENSE-2.0
7Unless required by applicable law or agreed to in writing, software
8distributed under the License is distributed on an "AS IS" BASIS,
9WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10See the License for the specific language governing permissions and
11limitations under the License.
12*/
13
14package commands
15
16import (
17	"context"
18	"encoding/json"
19	"errors"
20	"fmt"
21	"os"
22	"path/filepath"
23	"sort"
24	"strconv"
25	"strings"
26	"time"
27
28	"github.com/blang/semver"
29	"github.com/digitalocean/doctl"
30	"github.com/digitalocean/doctl/commands/displayers"
31	"github.com/digitalocean/doctl/do"
32	"github.com/digitalocean/godo"
33	"github.com/google/uuid"
34	"github.com/spf13/cobra"
35	"github.com/spf13/viper"
36
37	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38	kubeerrors "k8s.io/apimachinery/pkg/util/errors"
39	clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
40	"k8s.io/client-go/tools/clientcmd"
41	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
42)
43
44const (
45	maxAPIFailures            = 5
46	timeoutFetchingKubeconfig = 30 * time.Second
47
48	defaultKubernetesNodeSize      = "s-1vcpu-2gb"
49	defaultKubernetesNodeCount     = 3
50	defaultKubernetesRegion        = "nyc1"
51	defaultKubernetesLatestVersion = "latest"
52
53	execCredentialKind = "ExecCredential"
54
55	workflowDesc = `
56
57A typical workflow is to use ` + "`" + `doctl kubernetes cluster create` + "`" + ` to create the cluster on DigitalOcean's infrastructure, then call ` + "`" + `doctl kubernetes cluster kubeconfig` + "`" + ` to configure ` + "`" + `kubectl` + "`" + ` to connect to the cluster. You are then able to use ` + "`" + `kubectl` + "`" + ` to create and manage workloads.`
58	optionsDesc = `
59
60The commands under ` + "`" + `doctl kubernetes options` + "`" + ` retrieve values used while creating clusters, such as the list of regions where cluster creation is supported.`
61)
62
63var getCurrentAuthContextFn = defaultGetCurrentAuthContextFn
64
65func defaultGetCurrentAuthContextFn() string {
66	if Context != "" {
67		return Context
68	}
69	if authContext := viper.GetString("context"); authContext != "" {
70		return authContext
71	}
72	return doctl.ArgDefaultContext
73}
74
75func errNoClusterByName(name string) error {
76	return fmt.Errorf("no cluster goes by the name %q", name)
77}
78
79func errAmbiguousClusterName(name string, ids []string) error {
80	return fmt.Errorf("many clusters go by the name %q, they have the following IDs: %v", name, ids)
81}
82
83func errNoPoolByName(name string) error {
84	return fmt.Errorf("No node pool goes by the name %q", name)
85}
86
87func errAmbiguousPoolName(name string, ids []string) error {
88	return fmt.Errorf("Many node pools go by the name %q, they have the following IDs: %v", name, ids)
89}
90
91func errNoClusterNodeByName(name string) error {
92	return fmt.Errorf("No node goes by the name %q", name)
93}
94
95func errAmbiguousClusterNodeName(name string, ids []string) error {
96	return fmt.Errorf("Many nodes go by the name %q, they have the following IDs: %v", name, ids)
97}
98
99// Kubernetes creates the kubernetes command.
100func Kubernetes() *Command {
101	cmd := &Command{
102		Command: &cobra.Command{
103			Use:     "kubernetes",
104			Aliases: []string{"kube", "k8s", "k"},
105			Short:   "Displays commands to manage Kubernetes clusters and configurations",
106			Long:    "The commands under `doctl kubernetes` are for managing Kubernetes clusters and viewing configuration options relating to clusters." + workflowDesc + optionsDesc,
107		},
108	}
109
110	cmd.AddCommand(kubernetesCluster())
111	cmd.AddCommand(kubernetesOptions())
112	cmd.AddCommand(kubernetesOneClicks())
113
114	return cmd
115}
116
117// KubeconfigProvider allows a user to read from a remote and local Kubeconfig, and write to a
118// local Kubeconfig.
119type KubeconfigProvider interface {
120	Remote(kube do.KubernetesService, clusterID string, expirySeconds int) (*clientcmdapi.Config, error)
121	Local() (*clientcmdapi.Config, error)
122	Write(config *clientcmdapi.Config) error
123	ConfigPath() string
124}
125
126type kubeconfigProvider struct {
127	pathOptions *clientcmd.PathOptions
128}
129
130// Remote returns the kubeconfig for the cluster with the given ID from DOKS.
131func (p *kubeconfigProvider) Remote(kube do.KubernetesService, clusterID string, expirySeconds int) (*clientcmdapi.Config, error) {
132	var kubeconfig []byte
133	var err error
134	if expirySeconds > 0 {
135		kubeconfig, err = kube.GetKubeConfigWithExpiry(clusterID, int64(expirySeconds))
136	} else {
137		kubeconfig, err = kube.GetKubeConfig(clusterID)
138	}
139	if err != nil {
140		return nil, err
141	}
142
143	return clientcmd.Load(kubeconfig)
144}
145
146// Local reads the kubeconfig from the user's local kubeconfig file.
147func (p *kubeconfigProvider) Local() (*clientcmdapi.Config, error) {
148	config, err := p.pathOptions.GetStartingConfig()
149	if err != nil {
150		if a, ok := err.(kubeerrors.Aggregate); ok {
151			_, isSnap := os.LookupEnv("SNAP")
152
153			for _, err := range a.Errors() {
154				// this should NOT be a contains check but they are formatting the
155				// error without implementing an unwrap (so the original permission
156				// error type is lost).
157				if strings.Contains(err.Error(), "permission denied") && isSnap {
158					warn("Using the doctl Snap? Grant access to the doctl:kube-config plug to use this command with: sudo snap connect doctl:kube-config")
159					return nil, err
160				}
161
162			}
163		}
164
165		return nil, err
166	}
167
168	return config, nil
169}
170
171// Write either writes to or updates an existing local kubeconfig file.
172func (p *kubeconfigProvider) Write(config *clientcmdapi.Config) error {
173	err := clientcmd.ModifyConfig(p.pathOptions, *config, false)
174	if err != nil {
175		_, ok := os.LookupEnv("SNAP")
176
177		if os.IsPermission(err) && ok {
178			warn("Using the doctl Snap? Grant access to the doctl:kube-config plug to use this command with: sudo snap connect doctl:kube-config")
179		}
180
181		return err
182	}
183
184	return nil
185}
186
187func (p *kubeconfigProvider) ConfigPath() string {
188	path := p.pathOptions.GetDefaultFilename()
189
190	if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) {
191		if _, ok := os.LookupEnv("SNAP"); ok {
192			warn("Using the doctl Snap? Please create the directory: %q before trying again", filepath.Dir(path))
193		}
194	}
195
196	return path
197}
198
199// KubernetesCommandService is used to execute Kubernetes commands.
200type KubernetesCommandService struct {
201	KubeconfigProvider KubeconfigProvider
202}
203
204func kubernetesCommandService() *KubernetesCommandService {
205	return &KubernetesCommandService{
206		KubeconfigProvider: &kubeconfigProvider{
207			pathOptions: clientcmd.NewDefaultPathOptions(),
208		},
209	}
210}
211
212func kubernetesCluster() *Command {
213	cmd := &Command{
214		Command: &cobra.Command{
215			Use:     "cluster",
216			Aliases: []string{"clusters", "c"},
217			Short:   "Display commands for managing Kubernetes clusters",
218			Long:    "The commands under `doctl kubernetes cluster` are for the management of Kubernetes clusters." + workflowDesc,
219		},
220	}
221
222	k8sCmdService := kubernetesCommandService()
223
224	cmd.AddCommand(kubernetesKubeconfig())
225
226	cmd.AddCommand(kubernetesNodePools())
227
228	cmd.AddCommand(kubernetesRegistryIntegration())
229
230	nodePoolDetails := `- A list of node pools available inside the cluster`
231	clusterDetails := `
232
233- A unique ID for the cluster
234- A human-readable name for the cluster
235- The slug identifier for the region where the Kubernetes cluster is located.
236- The slug identifier for the version of Kubernetes used for the cluster. If set to a minor version (e.g. ` + "`" + `1.14` + "`" + `), the latest version within it will be used (e.g. ` + "`" + `1.14.6-do.1` + "`" + `); if set to ` + "`" + `latest` + "`" + `, the latest published version will be used.
237- A boolean value indicating whether the cluster will be automatically upgraded to new patch releases during its maintenance window.
238- An object containing a "state" attribute whose value is set to a string indicating the current status of the node. Potential values include ` + "`" + `running` + "`" + `, ` + "`" + `provisioning` + "`" + `, and ` + "`" + `errored` + "`" + `.`
239	CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterGet, "get <id|name>", "Retrieve details about a Kubernetes cluster", `
240This command retrieves the following details about a Kubernetes cluster: `+clusterDetails+`
241- The base URL of the cluster's Kubernetes API server.
242- The public IPv4 address of the cluster's Kubernetes API server.
243- The range of IP addresses in the overlay network of the Kubernetes cluster in CIDR notation.
244- The range of assignable IP addresses for services running in the Kubernetes cluster in CIDR notation.
245- An array of tags applied to the Kubernetes cluster. All clusters are automatically tagged `+"`"+`k8s`+"`"+` and `+"`"+`k8s:$K8S_CLUSTER_ID`+"`"+`.
246- A time value given in ISO8601 combined date and time format that represents when the Kubernetes cluster was created.
247- A time value given in ISO8601 combined date and time format that represents when the Kubernetes cluster was last updated.
248`+nodePoolDetails,
249		Writer, aliasOpt("g"), displayerType(&displayers.KubernetesClusters{}))
250	CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterList, "list", "Retrieve the list of Kubernetes clusters for your account", `
251This command retrieves the following details about all Kubernetes clusters that are on your account:`+clusterDetails+nodePoolDetails,
252		Writer, aliasOpt("ls"), displayerType(&displayers.KubernetesClusters{}))
253	CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterGetUpgrades, "get-upgrades <id|name>",
254		"Retrieve a list of available Kubernetes version upgrades", `
255This command returns a list of slugs representing Kubernetes versions you can use with the specified cluster. You can use these values to upgrade your cluster with the `+"`"+`doctl kubernetes cluster upgrade`+"`"+` command.
256`, Writer, aliasOpt("gu"))
257
258	cmdKubeClusterCreate := CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterCreate(defaultKubernetesNodeSize,
259		defaultKubernetesNodeCount), "create <name>", "Create a Kubernetes cluster", `
260Creates a Kubernetes cluster given the specified options, using the specified name. Before creating the cluster, you can use `+"`"+`doctl kubernetes options`+"`"+` to see possible values for the various configuration flags.
261
262If no configuration flags are used, a three-node cluster with a single node pool will be created in the nyc1 region, using the latest Kubernetes version.
263
264After creating a cluster, a configuration context will be added to kubectl and made active so that you can begin managing your new cluster immediately.`,
265		Writer, aliasOpt("c"))
266	AddStringFlag(cmdKubeClusterCreate, doctl.ArgRegionSlug, "", defaultKubernetesRegion,
267		"Cluster region. Possible values: see `doctl kubernetes options regions`", requiredOpt())
268	AddStringFlag(cmdKubeClusterCreate, doctl.ArgClusterVersionSlug, "", "latest",
269		"Kubernetes version. Possible values: see `doctl kubernetes options versions`")
270	AddStringFlag(cmdKubeClusterCreate, doctl.ArgClusterVPCUUID, "", "",
271		"Kubernetes UUID. Must be the UUID of a valid VPC in the same region specified for the cluster.")
272	AddBoolFlag(cmdKubeClusterCreate, doctl.ArgAutoUpgrade, "", false,
273		"A boolean flag indicating whether the cluster will be automatically upgraded to new patch releases during its maintenance window (default false). To enable automatic upgrade, supply --auto-upgrade(=true).")
274	AddBoolFlag(cmdKubeClusterCreate, doctl.ArgSurgeUpgrade, "", true,
275		"Boolean specifying whether to enable surge-upgrade for the cluster")
276	AddBoolFlag(cmdKubeClusterCreate, doctl.ArgHA, "", false,
277		"A boolean flag indicating whether the cluster will be configured with a highly-available control plane (default false). To enable the HA control plane, supply --ha(=true).")
278	AddStringSliceFlag(cmdKubeClusterCreate, doctl.ArgTag, "", nil,
279		"Comma-separated list of tags to apply to the cluster, in addition to the default tags of `k8s` and `k8s:$K8S_CLUSTER_ID`.")
280	AddStringFlag(cmdKubeClusterCreate, doctl.ArgSizeSlug, "",
281		defaultKubernetesNodeSize,
282		"Machine size to use when creating nodes in the default node pool (incompatible with --"+doctl.ArgClusterNodePool+"). Possible values: see `doctl kubernetes options sizes`")
283	AddStringSliceFlag(cmdKubeClusterCreate, doctl.ArgOneClicks, "", nil, "Comma-separated list of 1-Click Applications to install on the kubernetes cluster. To see a list of 1-Click Applications available run doctl kubernetes 1-click list")
284	AddIntFlag(cmdKubeClusterCreate, doctl.ArgNodePoolCount, "",
285		defaultKubernetesNodeCount,
286		"Number of nodes in the default node pool (incompatible with --"+doctl.ArgClusterNodePool+")")
287	AddStringSliceFlag(cmdKubeClusterCreate, doctl.ArgClusterNodePool, "", nil,
288		`Comma-separated list of node pools, defined using semicolon-separated configuration values and surrounded by quotes (incompatible with --`+doctl.ArgSizeSlug+` and --`+doctl.ArgNodePoolCount+`)
289Format: `+"`"+`"name=your-name;size=size_slug;count=5;tag=tag1;tag=tag2;label=key1=value1;label=key2=label2;taint=key1=value1:NoSchedule;taint=key2:NoExecute"`+"`"+` where:
290
291- `+"`"+`name`+"`"+`: Name of the node pool, which must be unique in the cluster
292- `+"`"+`size`+"`"+`: Machine size of the nodes to use. Possible values: see `+"`"+`doctl kubernetes options sizes`+"`"+`.
293- `+"`"+`count`+"`"+`: Number of nodes to create.
294- `+"`"+`tag`+"`"+`: Comma-separated list of tags to apply to nodes in the pool
295- `+"`"+`label`+"`"+`: Label in key=value notation; repeat to add multiple labels at once.
296- `+"`"+`taint`+"`"+`: Taint in key[=value]:effect notation; repeat to add multiple taints at once.
297- `+"`"+`auto-scale`+"`"+`: Boolean defining whether to enable cluster auto-scaling on the node pool.
298- `+"`"+`min-nodes`+"`"+`: Minimum number of nodes that can be auto-scaled to.
299- `+"`"+`max-nodes`+"`"+`: Maximum number of nodes that can be auto-scaled to.`)
300
301	AddBoolFlag(cmdKubeClusterCreate, doctl.ArgClusterUpdateKubeconfig, "", true,
302		"Boolean that specifies whether to add a configuration context for the new cluster to your kubectl")
303	AddBoolFlag(cmdKubeClusterCreate, doctl.ArgCommandWait, "", true,
304		"Boolean that specifies whether to wait for cluster creation to complete before returning control to the terminal")
305	AddBoolFlag(cmdKubeClusterCreate, doctl.ArgSetCurrentContext, "", true,
306		"Boolean that specifies whether to set the current kubectl context to that of the new cluster")
307	AddStringFlag(cmdKubeClusterCreate, doctl.ArgMaintenanceWindow, "", "any=00:00",
308		"Sets the beginning of the four hour maintenance window for the cluster. Syntax is in the format: `day=HH:MM`, where time is in UTC. Day can be: `any`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday"+"`.")
309
310	cmdKubeClusterUpdate := CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterUpdate, "update <id|name>",
311		"Update a Kubernetes cluster's configuration", `
312This command updates the specified configuration values for the specified Kubernetes cluster. The cluster must be referred to by its name or ID, which you can retrieve by calling:
313
314	doctl kubernetes cluster list`, Writer, aliasOpt("u"))
315	AddStringFlag(cmdKubeClusterUpdate, doctl.ArgClusterName, "", "",
316		"Specifies a new cluster name")
317	AddStringSliceFlag(cmdKubeClusterUpdate, doctl.ArgTag, "", nil,
318		"A comma-separated list of tags to apply to the cluster. Existing user-provided tags will be removed from the cluster if they are not specified.")
319	AddBoolFlag(cmdKubeClusterUpdate, doctl.ArgAutoUpgrade, "", false,
320		"A boolean flag indicating whether the cluster will be automatically upgraded to new patch releases during its maintenance window (default false). To enable automatic upgrade, supply --auto-upgrade(=true).")
321	AddBoolFlag(cmdKubeClusterUpdate, doctl.ArgSurgeUpgrade, "", false,
322		"Boolean specifying whether to enable surge-upgrade for the cluster")
323	AddBoolFlag(cmdKubeClusterUpdate, doctl.ArgClusterUpdateKubeconfig, "",
324		true, "Boolean specifying whether to update the cluster in your kubeconfig")
325	AddBoolFlag(cmdKubeClusterUpdate, doctl.ArgSetCurrentContext, "", true,
326		"Boolean specifying whether to set the current kubectl context to that of the new cluster")
327	AddStringFlag(cmdKubeClusterUpdate, doctl.ArgMaintenanceWindow, "", "any=00:00",
328		"Sets the beginning of the four hour maintenance window for the cluster. Syntax is in the format: 'day=HH:MM', where time is in UTC. Day can be: `any`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday"+"`.")
329
330	cmdKubeClusterUpgrade := CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterUpgrade,
331		"upgrade <id|name>", "Upgrades a cluster to a new Kubernetes version", `
332
333This command upgrades the specified Kubernetes cluster. By default, this will upgrade the cluster to the latest available release, but you can also specify any version listed for your cluster by using `+"`"+`doctl k8s get-upgrades`+"`"+`.`, Writer)
334	AddStringFlag(cmdKubeClusterUpgrade, doctl.ArgClusterVersionSlug, "", "latest",
335		`The desired Kubernetes version. Possible values: see `+"`"+`doctl k8s get-upgrades <cluster>`+"`"+`.
336The special value `+"`"+`latest`+"`"+` will select the most recent patch version for your cluster's minor version.
337For example, if a cluster is on 1.12.1 and upgrades are available to 1.12.3 and 1.13.1, 1.12.3 will be `+"`"+`latest`+"`"+`.`)
338
339	cmdKubeClusterDelete := CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterDelete,
340		"delete <id|name>...", "Delete Kubernetes clusters ", `
341This command deletes the specified Kubernetes clusters and the Droplets associated with them. To delete all other DigitalOcean resources created during the operation of the clusters, such as load balancers, volumes or volume snapshots, use the --dangerous flag.
342`, Writer, aliasOpt("d", "rm"))
343	AddBoolFlag(cmdKubeClusterDelete, doctl.ArgForce, doctl.ArgShortForce, false,
344		"Boolean indicating whether to delete the cluster without a confirmation prompt")
345	AddBoolFlag(cmdKubeClusterDelete, doctl.ArgClusterUpdateKubeconfig, "", true,
346		"Boolean indicating whether to remove the deleted cluster from your kubeconfig")
347	AddBoolFlag(cmdKubeClusterDelete, doctl.ArgDangerous, "", false,
348		"Boolean indicating whether to delete the cluster's associated resources like load balancers, volumes and volume snapshots")
349
350	cmdKubeClusterDeleteSelective := CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterDeleteSelective,
351		"delete-selective <id|name>", "Delete a Kubernetes cluster and selectively delete resources associated with it", `
352This command deletes the specified Kubernetes cluster and droplets associated with it. It also deletes the specified associated resources. The associated resources supported for selective deletion are load balancers, volumes and volume snapshots.
353`, Writer, aliasOpt("ds"))
354	AddBoolFlag(cmdKubeClusterDeleteSelective, doctl.ArgForce, doctl.ArgShortForce, false,
355		"Boolean indicating whether to delete the cluster without a confirmation prompt")
356	AddBoolFlag(cmdKubeClusterDeleteSelective, doctl.ArgClusterUpdateKubeconfig, "", true,
357		"Boolean indicating whether to remove the deleted cluster from your kubeconfig")
358	AddStringSliceFlag(cmdKubeClusterDeleteSelective, doctl.ArgVolumeList, "", nil,
359		"Comma-separated list of volume IDs or names for deletion")
360	AddStringSliceFlag(cmdKubeClusterDeleteSelective, doctl.ArgVolumeSnapshotList, "", nil,
361		"Comma-separated list of volume snapshot IDs or names for deletion")
362	AddStringSliceFlag(cmdKubeClusterDeleteSelective, doctl.ArgLoadBalancerList, "", nil,
363		"Comma-separated list of load balancer IDs or names for deletion")
364
365	CmdBuilder(cmd, k8sCmdService.RunKubernetesClusterListAssociatedResources, "list-associated-resources <id|name>", "Retrieve DigitalOcean resources associated with a Kubernetes cluster", `
366This command retrieves the following details:
367- Volume IDs for volumes created by the DigitalOcean CSI driver
368- Volume snapshot IDs for volume snapshots created by the DigitalOcean CSI driver.
369- Load balancer IDs for load balancers managed by the Kubernetes cluster.`,
370		Writer, aliasOpt("ar"), displayerType(&displayers.KubernetesAssociatedResources{}))
371
372	return cmd
373}
374
375func kubernetesKubeconfig() *Command {
376	cmd := &Command{
377		Command: &cobra.Command{
378			Use:     "kubeconfig",
379			Aliases: []string{"kubecfg", "k8scfg", "config", "cfg"},
380			Short:   "Display commands for managing your local kubeconfig",
381			Long:    "The commands under `doctl kubernetes cluster kubeconfig` are used to manage Kubernetes cluster credentials on your local machine. The credentials are used as authentication contexts with `kubectl`, the Kubernetes command-line interface.",
382		},
383	}
384
385	k8sCmdService := kubernetesCommandService()
386
387	cmdShowConfig := CmdBuilder(cmd, k8sCmdService.RunKubernetesKubeconfigShow, "show <cluster-id|cluster-name>", "Show a Kubernetes cluster's kubeconfig YAML", `
388This command prints out the raw YAML for the specified cluster's kubeconfig.	`, Writer, aliasOpt("p", "g"))
389	AddIntFlag(cmdShowConfig, doctl.ArgKubeConfigExpirySeconds, "", 0,
390		"The length of time the cluster credentials will be valid for in seconds. By default, the credentials expire after seven days.")
391
392	execCredDesc := "INTERNAL: This hidden command is for printing a cluster's exec credential"
393	cmdExecCredential := CmdBuilder(cmd, k8sCmdService.RunKubernetesKubeconfigExecCredential, "exec-credential <cluster-id>", execCredDesc, execCredDesc, Writer, hiddenCmd())
394	AddStringFlag(cmdExecCredential, doctl.ArgVersion, "", "", "")
395
396	cmdSaveConfig := CmdBuilder(cmd, k8sCmdService.RunKubernetesKubeconfigSave, "save <cluster-id|cluster-name>", "Save a cluster's credentials to your local kubeconfig", `
397This command adds the credentials for the specified cluster to your local kubeconfig. After this, your kubectl installation can directly manage the specified cluster.
398		`, Writer, aliasOpt("s"))
399	AddBoolFlag(cmdSaveConfig, doctl.ArgSetCurrentContext, "", true, "Boolean indicating whether to set the current kubectl context to that of the new cluster")
400	AddIntFlag(cmdSaveConfig, doctl.ArgKubeConfigExpirySeconds, "", 0,
401		"The length of time the cluster credentials will be valid for in seconds. By default, the credentials are automatically renewed as needed.")
402
403	CmdBuilder(cmd, k8sCmdService.RunKubernetesKubeconfigRemove, "remove <cluster-id|cluster-name>", "Remove a cluster's credentials from your local kubeconfig", `
404This command removes the specified cluster's credentials from your local kubeconfig. After running this command, you will not be able to use `+"`"+`kubectl`+"`"+` to interact with your cluster.
405`, Writer, aliasOpt("d", "rm"))
406	return cmd
407}
408
409func kubeconfigCachePath() string {
410	return filepath.Join(defaultConfigHome(), "cache", "exec-credential")
411}
412
413func kubernetesNodePools() *Command {
414	cmd := &Command{
415		Command: &cobra.Command{
416			Use:     "node-pool",
417			Aliases: []string{"node-pools", "nodepool", "nodepools", "pool", "pools", "np", "p"},
418			Short:   "Display commands for managing node pools",
419			Long:    "The commands under `node-pool` are for performing actions on a Kubernetes cluster's node pools. You can use these commands to create or delete node pools, enable autoscaling for a node pool, and more.",
420		},
421	}
422
423	k8sCmdService := kubernetesCommandService()
424
425	CmdBuilder(cmd, k8sCmdService.RunKubernetesNodePoolGet, "get <cluster-id|cluster-name> <pool-id|pool-name>",
426		"Retrieve information about a cluster's node pool", `
427This command retrieves information about the specified node pool in the specified cluster, including:
428
429- The node pool ID
430- The machine size of the nodes (e.g. `+"`"+`s-1vcpu-2gb`+"`"+`)
431- The number of nodes in the pool
432- Tags applied to the node pool
433- The names of the nodes
434
435Specifying `+"`"+`--output=json`+"`"+` when calling this command will produce extra information about the individual nodes in the response, such as their IDs, status, creation time, and update time.
436`, Writer, aliasOpt("g"),
437		displayerType(&displayers.KubernetesNodePools{}))
438	CmdBuilder(cmd, k8sCmdService.RunKubernetesNodePoolList, "list <cluster-id|cluster-name>",
439		"List a cluster's node pools", `
440This command retrieves information about the specified cluster's node pools, including:
441
442- The node pool ID
443- The machine size of the nodes (e.g. `+"`"+`s-1vcpu-2gb`+"`"+`)
444- The number of nodes in the pool
445- Tags applied to the node pool
446- The names of the nodes
447
448Specifying `+"`"+`--output=json`+"`"+` when calling this command will produce extra information about the individual nodes in the response, such as their IDs, status, creation time, and update time.
449		`, Writer, aliasOpt("ls"),
450		displayerType(&displayers.KubernetesNodePools{}))
451
452	cmdKubeNodePoolCreate := CmdBuilder(cmd, k8sCmdService.RunKubernetesNodePoolCreate,
453		"create <cluster-id|cluster-name>", "Create a new node pool for a cluster", `
454This command creates a new node pool for the specified cluster. At a minimum, you'll need to specify the size of the nodes, and the number of nodes to place in the pool. You can also specify that you'd like to enable autoscaling and set minimum and maximum node poll sizes.
455		`,
456		Writer, aliasOpt("c"))
457	AddStringFlag(cmdKubeNodePoolCreate, doctl.ArgNodePoolName, "", "",
458		"Name of the node pool", requiredOpt())
459	AddStringFlag(cmdKubeNodePoolCreate, doctl.ArgSizeSlug, "", "",
460		"Size of the nodes in the node pool (To see possible values: call `doctl kubernetes options sizes`)", requiredOpt())
461	AddIntFlag(cmdKubeNodePoolCreate, doctl.ArgNodePoolCount, "", 0,
462		"The size of (number of nodes in) the node pool", requiredOpt())
463	AddStringSliceFlag(cmdKubeNodePoolCreate, doctl.ArgTag, "", nil,
464		"Tag to apply to the node pool; repeat to specify additional tags. An existing tag is removed from the node pool if it is not specified by any flag.")
465	AddStringSliceFlag(cmdKubeNodePoolCreate, doctl.ArgKubernetesLabel, "", nil,
466		"Label in key=value notation to apply to the node pool; repeat to specify additional labels. An existing label is removed from the node pool if it is not specified by any flag.")
467	AddStringSliceFlag(cmdKubeNodePoolCreate, doctl.ArgKubernetesTaint, "", nil,
468		"Taint in key[=value:]effect notation to apply to the node pool; repeat to specify additional taints. Set to the empty string \"\" to clear all taints. An existing taint is removed from the node pool if it is not specified by any flag.")
469	AddBoolFlag(cmdKubeNodePoolCreate, doctl.ArgNodePoolAutoScale, "", false,
470		"Boolean indicating whether to enable auto-scaling on the node pool")
471	AddIntFlag(cmdKubeNodePoolCreate, doctl.ArgNodePoolMinNodes, "", 0,
472		"Minimum number of nodes in the node pool when autoscaling is enabled")
473	AddIntFlag(cmdKubeNodePoolCreate, doctl.ArgNodePoolMaxNodes, "", 0,
474		"Maximum number of nodes in the node pool when autoscaling is enabled")
475
476	cmdKubeNodePoolUpdate := CmdBuilder(cmd, k8sCmdService.RunKubernetesNodePoolUpdate,
477		"update <cluster-id|cluster-name> <pool-id|pool-name>",
478		"Update an existing node pool in a cluster", "This command updates the specified node pool in the specified cluster. You can update any value for which there is a flag.", Writer, aliasOpt("u"))
479	AddStringFlag(cmdKubeNodePoolUpdate, doctl.ArgNodePoolName, "", "", "Name of the node pool")
480	AddIntFlag(cmdKubeNodePoolUpdate, doctl.ArgNodePoolCount, "", 0,
481		"The size of (number of nodes in) the node pool")
482	AddStringSliceFlag(cmdKubeNodePoolUpdate, doctl.ArgTag, "", nil,
483		"Tag to apply to the node pool; you can supply multiple `--tag` arguments to specify additional tags. Omitted tags will be removed from the node pool if the flag is specified.")
484	AddStringSliceFlag(cmdKubeNodePoolUpdate, doctl.ArgKubernetesLabel, "", nil,
485		"Label in key=value notation to apply to the node pool, repeat to add multiple labels at once. Omitted labels will be removed from the node pool if the flag is specified.")
486	AddStringSliceFlag(cmdKubeNodePoolUpdate, doctl.ArgKubernetesTaint, "", nil,
487		"Taint in key[=value:]effect notation to apply to the node pool, repeat to add multiple taints at once. Omitted taints will be removed from the node pool if the flag is specified.")
488	AddBoolFlag(cmdKubeNodePoolUpdate, doctl.ArgNodePoolAutoScale, "", false,
489		"Boolean indicating whether to enable auto-scaling on the node pool")
490	AddIntFlag(cmdKubeNodePoolUpdate, doctl.ArgNodePoolMinNodes, "", 0,
491		"Minimum number of nodes in the node pool when autoscaling is enabled")
492	AddIntFlag(cmdKubeNodePoolUpdate, doctl.ArgNodePoolMaxNodes, "", 0,
493		"Maximum number of nodes in the node pool when autoscaling is enabled")
494
495	recycleDesc := "DEPRECATED: Use `replace-node`. Recycle nodes in a node pool"
496	cmdKubeNodePoolRecycle := CmdBuilder(cmd, k8sCmdService.RunKubernetesNodePoolRecycle,
497		"recycle <cluster-id|cluster-name> <pool-id|pool-name>", recycleDesc, recycleDesc, Writer, aliasOpt("r"), hiddenCmd())
498	AddStringFlag(cmdKubeNodePoolRecycle, doctl.ArgNodePoolNodeIDs, "", "",
499		"ID or name of the nodes in the node pool to recycle")
500
501	cmdKubeNodePoolDelete := CmdBuilder(cmd, k8sCmdService.RunKubernetesNodePoolDelete,
502		"delete <cluster-id|cluster-name> <pool-id|pool-name>",
503		"Delete a node pool", `This command deletes the specified node pool in the specified cluster, which also removes all the nodes inside that pool. This action is irreversable.`, Writer, aliasOpt("d", "rm"))
504	AddBoolFlag(cmdKubeNodePoolDelete, doctl.ArgForce, doctl.ArgShortForce,
505		false, "Delete node pool without confirmation prompt")
506
507	cmdKubeNodeDelete := CmdBuilder(cmd, k8sCmdService.RunKubernetesNodeDelete, "delete-node <cluster-id|cluster-name> <pool-id|pool-name> <node-id>", "Delete a node", `
508This command deletes the specified node, located in the specified node pool. By default this deletion will happen gracefully, and Kubernetes will drain the node of any pods before deleting it.
509		`, Writer)
510	AddBoolFlag(cmdKubeNodeDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the node without a confirmation prompt")
511	AddBoolFlag(cmdKubeNodeDelete, "skip-drain", "", false, "Skip draining the node before deletion")
512
513	cmdKubeNodeReplace := CmdBuilder(cmd, k8sCmdService.RunKubernetesNodeReplace, "replace-node <cluster-id|cluster-name> <pool-id|pool-name> <node-id>", "Replace node with a new one", `
514This command deletes the specified node in the specified node pool, and then creates a new node in its place. This is useful if you suspect a node has entered an undesired state. By default the deletion will happen gracefully, and Kubernetes will drain the node of any pods before deleting it.
515		`, Writer)
516	AddBoolFlag(cmdKubeNodeReplace, doctl.ArgForce, doctl.ArgShortForce, false, "Replace node without confirmation prompt")
517	AddBoolFlag(cmdKubeNodeReplace, "skip-drain", "", false, "Skip draining the node before replacement")
518
519	return cmd
520}
521
522func kubernetesRegistryIntegration() *Command {
523	cmd := &Command{
524		Command: &cobra.Command{
525			Use:     "registry",
526			Aliases: []string{"reg"},
527			Short:   "Display commands for integrating clusters with docr",
528			Long:    "The commands under `registry` are for managing DOCR integration with Kubernetes clusters. You can use these commands to add or remove registry from one or more clusters.",
529		},
530	}
531
532	k8sCmdService := kubernetesCommandService()
533
534	CmdBuilder(cmd, k8sCmdService.RunKubernetesRegistryAdd,
535		"add <cluster-id|cluster-name> <cluster-id|cluster-name>", "Add container registry support to Kubernetes clusters", `
536This command adds container registry support to the specified Kubernetes cluster(s).`,
537		Writer, aliasOpt("a"))
538
539	CmdBuilder(cmd, k8sCmdService.RunKubernetesRegistryRemove,
540		"remove <cluster-id|cluster-name> <cluster-id|cluster-name>", "Remove container registry support from Kubernetes clusters", `
541This command removes container registry support from the specified Kubernetes cluster(s).`,
542		Writer, aliasOpt("rm"))
543
544	return cmd
545}
546
547// kubernetesOneClicks creates the 1-click command.
548func kubernetesOneClicks() *Command {
549	cmd := &Command{
550		Command: &cobra.Command{
551			Use:   "1-click",
552			Short: "Display commands that pertain to kubernetes 1-click applications",
553			Long:  "The commands under `doctl kubernetes 1-click` are for interacting with DigitalOcean Kubernetes 1-Click applications.",
554		},
555	}
556
557	CmdBuilder(cmd, RunKubernetesOneClickList, "list", "Retrieve a list of Kubernetes 1-Click applications", "Use this command to retrieve a list of Kubernetes 1-Click applications.", Writer,
558		aliasOpt("ls"), displayerType(&displayers.OneClick{}))
559	cmdKubeOneClickInstall := CmdBuilder(cmd, RunKubernetesOneClickInstall, "install <cluster-id>", "Install 1-click apps on a Kubernetes cluster", "Use this command to install 1-click apps on a Kubernetes cluster using the flag --1-clicks.", Writer, aliasOpt("in"), displayerType(&displayers.OneClick{}))
560	AddStringSliceFlag(cmdKubeOneClickInstall, doctl.ArgOneClicks, "", nil, "1-clicks to be installed on a Kubernetes cluster. Multiple 1-clicks can be added at once. Example: --1-clicks moon,loki,netdata")
561	return cmd
562}
563
564// RunKubernetesOneClickList retrieves a list of 1-clicks for kubernetes.
565func RunKubernetesOneClickList(c *CmdConfig) error {
566	oneClicks := c.OneClicks()
567	oneClickList, err := oneClicks.List("kubernetes")
568	if err != nil {
569		return err
570	}
571
572	items := &displayers.OneClick{OneClicks: oneClickList}
573
574	return c.Display(items)
575}
576
577// RunKubernetesOneClickInstall installs 1-click apps on a kubernetes cluster.
578func RunKubernetesOneClickInstall(c *CmdConfig) error {
579	oneClicks := c.OneClicks()
580	if len(c.Args) < 1 {
581		return doctl.NewMissingArgsErr(c.NS)
582	}
583
584	oneClickSlice, err := c.Doit.GetStringSlice(c.NS, doctl.ArgOneClicks)
585	if err != nil {
586		return err
587	}
588
589	oneClickInstall, err := oneClicks.InstallKubernetes(c.Args[0], oneClickSlice)
590	if err != nil {
591		return err
592	}
593
594	notice(oneClickInstall)
595	return nil
596}
597
598func kubernetesOptions() *Command {
599	cmd := &Command{
600		Command: &cobra.Command{
601			Use:     "options",
602			Aliases: []string{"opts", "o"},
603			Short:   "List possible option values for use inside Kubernetes commands",
604			Long:    "The `options` commands are used to enumerate values for use with `doctl`'s Kubernetes commands. This is useful in certain cases where flags only accept input that is from a list of possible values, such as Kubernetes versions, datacenter regions, and machine sizes.",
605		},
606	}
607
608	k8sCmdService := kubernetesCommandService()
609
610	k8sVersionDesc := "List Kubernetes versions that can be used with DigitalOcean clusters"
611	CmdBuilder(cmd, k8sCmdService.RunKubeOptionsListVersion, "versions",
612		k8sVersionDesc, k8sVersionDesc, Writer, aliasOpt("v"))
613	k8sRegionsDesc := "List regions that support DigitalOcean Kubernetes clusters"
614	CmdBuilder(cmd, k8sCmdService.RunKubeOptionsListRegion, "regions",
615		k8sRegionsDesc, k8sRegionsDesc, Writer, aliasOpt("r"))
616	k8sSizesDesc := "List machine sizes that can be used in a DigitalOcean Kubernetes cluster"
617	CmdBuilder(cmd, k8sCmdService.RunKubeOptionsListNodeSizes, "sizes",
618		k8sSizesDesc, k8sSizesDesc, Writer, aliasOpt("s"))
619	return cmd
620}
621
622// Clusters
623
624// RunKubernetesClusterGet retrieves an existing kubernetes cluster by its identifier.
625func (s *KubernetesCommandService) RunKubernetesClusterGet(c *CmdConfig) error {
626	err := ensureOneArg(c)
627	if err != nil {
628		return err
629	}
630	clusterIDorName := c.Args[0]
631
632	cluster, err := clusterByIDorName(c.Kubernetes(), clusterIDorName)
633	if err != nil {
634		return err
635	}
636	return displayClusters(c, false, *cluster)
637}
638
639// RunKubernetesClusterList lists kubernetes.
640func (s *KubernetesCommandService) RunKubernetesClusterList(c *CmdConfig) error {
641	kube := c.Kubernetes()
642	list, err := kube.List()
643	if err != nil {
644		return err
645	}
646
647	return displayClusters(c, true, list...)
648}
649
650// RunKubernetesClusterGetUpgrades retrieves available upgrade versions for a cluster.
651func (s *KubernetesCommandService) RunKubernetesClusterGetUpgrades(c *CmdConfig) error {
652	err := ensureOneArg(c)
653	if err != nil {
654		return err
655	}
656	clusterIDorName := c.Args[0]
657	clusterID, err := clusterIDize(c, clusterIDorName)
658	if err != nil {
659		return err
660	}
661
662	kube := c.Kubernetes()
663
664	upgrades, err := kube.GetUpgrades(clusterID)
665	if err != nil {
666		return err
667	}
668
669	item := &displayers.KubernetesVersions{KubernetesVersions: upgrades}
670	return c.Display(item)
671}
672
673// RunKubernetesClusterCreate creates a new kubernetes with a given configuration.
674func (s *KubernetesCommandService) RunKubernetesClusterCreate(defaultNodeSize string, defaultNodeCount int) func(*CmdConfig) error {
675	return func(c *CmdConfig) error {
676		err := ensureOneArg(c)
677		if err != nil {
678			return err
679		}
680		clusterName := c.Args[0]
681		r := &godo.KubernetesClusterCreateRequest{Name: clusterName}
682		if err := buildClusterCreateRequestFromArgs(c, r, defaultNodeSize, defaultNodeCount); err != nil {
683			return err
684		}
685		wait, err := c.Doit.GetBool(c.NS, doctl.ArgCommandWait)
686		if err != nil {
687			return err
688		}
689		update, err := c.Doit.GetBool(c.NS, doctl.ArgClusterUpdateKubeconfig)
690		if err != nil {
691			return err
692		}
693		setCurrentContext, err := c.Doit.GetBool(c.NS, doctl.ArgSetCurrentContext)
694		if err != nil {
695			return err
696		}
697
698		kube := c.Kubernetes()
699
700		cluster, err := kube.Create(r)
701		if err != nil {
702			return err
703		}
704
705		if wait {
706			notice("Cluster is provisioning, waiting for cluster to be running")
707			cluster, err = waitForClusterRunning(kube, cluster.ID)
708			if err != nil {
709				warn("Cluster couldn't enter `running` state: %v", err)
710			}
711		}
712
713		if update {
714			notice("Cluster created, fetching credentials")
715			s.tryUpdateKubeconfig(kube, cluster.ID, clusterName, setCurrentContext)
716		}
717
718		oneClickApps, err := c.Doit.GetStringSlice(c.NS, doctl.ArgOneClicks)
719		if err != nil {
720			return err
721		}
722		if len(oneClickApps) > 0 {
723			oneClicks := c.OneClicks()
724			messageResponse, err := oneClicks.InstallKubernetes(cluster.ID, oneClickApps)
725			if err != nil {
726				warn("Failed to kick off 1-Click Application(s) install")
727			} else {
728				notice(messageResponse)
729			}
730		}
731
732		return displayClusters(c, true, *cluster)
733	}
734}
735
736// RunKubernetesClusterUpdate updates an existing kubernetes with new configuration.
737func (s *KubernetesCommandService) RunKubernetesClusterUpdate(c *CmdConfig) error {
738	if len(c.Args) == 0 {
739		return doctl.NewMissingArgsErr(c.NS)
740	}
741	update, err := c.Doit.GetBool(c.NS, doctl.ArgClusterUpdateKubeconfig)
742	if err != nil {
743		return err
744	}
745	setCurrentContext, err := c.Doit.GetBool(c.NS, doctl.ArgSetCurrentContext)
746	if err != nil {
747		return err
748	}
749	clusterIDorName := c.Args[0]
750	clusterID, err := clusterIDize(c, clusterIDorName)
751	if err != nil {
752		return err
753	}
754
755	r := new(godo.KubernetesClusterUpdateRequest)
756	if err := buildClusterUpdateRequestFromArgs(c, r); err != nil {
757		return err
758	}
759
760	kube := c.Kubernetes()
761	cluster, err := kube.Update(clusterID, r)
762	if err != nil {
763		return err
764	}
765
766	if update {
767		notice("Cluster updated, fetching new credentials")
768		s.tryUpdateKubeconfig(kube, clusterID, clusterIDorName, setCurrentContext)
769	}
770
771	return displayClusters(c, true, *cluster)
772}
773
774func (s *KubernetesCommandService) tryUpdateKubeconfig(kube do.KubernetesService, clusterID, clusterName string, setCurrentContext bool) {
775	var (
776		remoteConfig *clientcmdapi.Config
777		err          error
778	)
779	ctx, cancel := context.WithTimeout(context.TODO(), timeoutFetchingKubeconfig)
780	defer cancel()
781	for {
782		remoteConfig, err = s.KubeconfigProvider.Remote(kube, clusterID, 0)
783		if err != nil {
784			select {
785			case <-ctx.Done():
786				warn("Couldn't get credentials for cluster. It will not be added to your kubeconfig: %v", err)
787				return
788			case <-time.After(time.Second):
789			}
790		} else {
791			break
792		}
793	}
794	if err := s.writeOrAddToKubeconfig(clusterID, remoteConfig, setCurrentContext, 0); err != nil {
795		warn("Couldn't write cluster credentials: %v", err)
796	}
797}
798
799// RunKubernetesClusterUpgrade upgrades an existing cluster to a new version.
800func (s *KubernetesCommandService) RunKubernetesClusterUpgrade(c *CmdConfig) error {
801	if len(c.Args) == 0 {
802		return doctl.NewMissingArgsErr(c.NS)
803	}
804	clusterID, err := clusterIDize(c, c.Args[0])
805	if err != nil {
806		return err
807	}
808
809	version, available, err := getUpgradeVersionOrLatest(c, clusterID)
810	if err != nil {
811		return err
812	}
813	if !available {
814		notice("Cluster is already up-to-date; no upgrades available.")
815		return nil
816	}
817
818	kube := c.Kubernetes()
819	err = kube.Upgrade(clusterID, version)
820	if err != nil {
821		return err
822	}
823
824	notice("Upgrading cluster to version %v", version)
825	return nil
826}
827
828func getUpgradeVersionOrLatest(c *CmdConfig, clusterID string) (string, bool, error) {
829	version, err := c.Doit.GetString(c.NS, doctl.ArgClusterVersionSlug)
830	if err != nil {
831		return "", false, err
832	}
833	if version != "" && version != defaultKubernetesLatestVersion {
834		return version, true, nil
835	}
836
837	cluster, err := c.Kubernetes().Get(clusterID)
838	if err != nil {
839		return "", false, fmt.Errorf("Unable to look up cluster to find the latest version from the API: %v", err)
840	}
841
842	versions, err := c.Kubernetes().GetUpgrades(clusterID)
843	if err != nil {
844		return "", false, fmt.Errorf("Unable to look up the latest version from the API: %v", err)
845	}
846	if len(versions) == 0 {
847		return "", false, nil
848	}
849
850	return latestVersionForUpgrade(cluster.VersionSlug, versions)
851}
852
853// latestVersionForUpgrade returns the newest patch version from `versions` for
854// the minor version of `clusterVersionSlug`. This ensures we never use a
855// different minor version than a cluster is running as "latest" for an upgrade,
856// since we want minor version upgrades to be an explicit operation.
857func latestVersionForUpgrade(clusterVersionSlug string, versions []do.KubernetesVersion) (string, bool, error) {
858	clusterSV, err := semver.Parse(clusterVersionSlug)
859	if err != nil {
860		return "", false, err
861	}
862	clusterBucket := fmt.Sprintf("%d.%d", clusterSV.Major, clusterSV.Minor)
863
864	// Sort releases into minor-version buckets.
865	var serr error
866	releases := versionMapBy(versions, func(v do.KubernetesVersion) string {
867		sv, err := semver.Parse(v.Slug)
868		if err != nil {
869			serr = err
870			return ""
871		}
872		return fmt.Sprintf("%d.%d", sv.Major, sv.Minor)
873	})
874	if serr != nil {
875		return "", false, serr
876	}
877
878	// Find the cluster's minor version in the bucketized available versions.
879	bucket, ok := releases[clusterBucket]
880	if !ok {
881		// No upgrades available within the cluster's minor version.
882		return "", false, nil
883	}
884
885	// Find the latest version within the bucket.
886	i, err := versionMaxBy(bucket, func(v do.KubernetesVersion) string {
887		return v.Slug
888	})
889	if err != nil {
890		return "", false, err
891	}
892
893	return bucket[i].Slug, true, nil
894}
895
896// RunKubernetesClusterDelete deletes a Kubernetes cluster
897func (s *KubernetesCommandService) RunKubernetesClusterDelete(c *CmdConfig) error {
898	if len(c.Args) < 1 {
899		return doctl.NewMissingArgsErr(c.NS)
900	}
901	update, err := c.Doit.GetBool(c.NS, doctl.ArgClusterUpdateKubeconfig)
902	if err != nil {
903		return err
904	}
905
906	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
907	if err != nil {
908		return err
909	}
910
911	dangerous, err := c.Doit.GetBool(c.NS, doctl.ArgDangerous)
912	if err != nil {
913		return err
914	}
915
916	kube := c.Kubernetes()
917
918	for _, cluster := range c.Args {
919		clusterID, err := clusterIDize(c, cluster)
920		if err != nil {
921			return err
922		}
923
924		if force || AskForConfirmDelete("Kubernetes cluster", 1) == nil {
925			// continue
926		} else {
927			return fmt.Errorf("Operation aborted")
928		}
929
930		var kubeconfig []byte
931		if update {
932			// get the cluster's kubeconfig before issuing the delete, so that we can
933			// cleanup the entry from the local file
934			kubeconfig, err = kube.GetKubeConfig(clusterID)
935			if err != nil {
936				warn("Couldn't get credentials for cluster. It will not be remove from your kubeconfig.")
937			}
938		}
939
940		if dangerous {
941			err = kube.DeleteDangerous(clusterID)
942		} else {
943			err = kube.Delete(clusterID)
944		}
945		if err != nil {
946			return err
947		}
948
949		if kubeconfig != nil {
950			notice("Cluster deleted, removing credentials")
951			if err := removeFromKubeconfig(kubeconfig); err != nil {
952				warn("Cluster was deleted, but we couldn't remove it from your local kubeconfig. Try doing it manually.")
953			}
954		}
955	}
956
957	return nil
958}
959
960func (s *KubernetesCommandService) RunKubernetesClusterDeleteSelective(c *CmdConfig) error {
961	err := ensureOneArg(c)
962	if err != nil {
963		return err
964	}
965	clusterIDorName := c.Args[0]
966
967	clusterID, err := clusterIDize(c, clusterIDorName)
968	if err != nil {
969		return err
970	}
971
972	update, err := c.Doit.GetBool(c.NS, doctl.ArgClusterUpdateKubeconfig)
973	if err != nil {
974		return err
975	}
976
977	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
978	if err != nil {
979		return err
980	}
981
982	volumes, err := c.Doit.GetStringSlice(c.NS, doctl.ArgVolumeList)
983	if err != nil {
984		return err
985	}
986
987	volSnapshots, err := c.Doit.GetStringSlice(c.NS, doctl.ArgVolumeSnapshotList)
988	if err != nil {
989		return err
990	}
991
992	loadBalancers, err := c.Doit.GetStringSlice(c.NS, doctl.ArgLoadBalancerList)
993	if err != nil {
994		return err
995	}
996
997	if force || AskForConfirmDelete("Kubernetes cluster", 1) == nil {
998		// continue
999	} else {
1000		return fmt.Errorf("Operation aborted")
1001	}
1002
1003	kube := c.Kubernetes()
1004
1005	var kubeconfig []byte
1006	if update {
1007		// get the cluster's kubeconfig before issuing the delete, so that we can
1008		// cleanup the entry from the local file
1009		kubeconfig, err = kube.GetKubeConfig(clusterID)
1010		if err != nil {
1011			warn("Couldn't get credentials for cluster. It will not be remove from your kubeconfig.")
1012		}
1013	}
1014
1015	cluster, err := kube.Get(clusterID)
1016	if err != nil {
1017		return err
1018	}
1019
1020	volIDs := make([]string, 0, len(volumes))
1021	for _, v := range volumes {
1022		volumeID, err := iDize(c, v, "volume", cluster.RegionSlug)
1023		if err != nil {
1024			return err
1025		}
1026		volIDs = append(volIDs, volumeID)
1027	}
1028
1029	snapshotIDs := make([]string, 0, len(volSnapshots))
1030	for _, s := range volSnapshots {
1031		snapID, err := iDize(c, s, "volume_snapshot", cluster.RegionSlug)
1032		if err != nil {
1033			return err
1034		}
1035		snapshotIDs = append(snapshotIDs, snapID)
1036	}
1037
1038	lbIDs := make([]string, 0, len(loadBalancers))
1039	for _, l := range loadBalancers {
1040		lbID, err := iDize(c, l, "load_balancer", "")
1041		if err != nil {
1042			return err
1043		}
1044		lbIDs = append(lbIDs, lbID)
1045	}
1046
1047	r := new(godo.KubernetesClusterDeleteSelectiveRequest)
1048	r.Volumes = volIDs
1049	r.VolumeSnapshots = snapshotIDs
1050	r.LoadBalancers = lbIDs
1051
1052	err = kube.DeleteSelective(clusterID, r)
1053	if err != nil {
1054		return err
1055	}
1056
1057	if kubeconfig != nil {
1058		notice("Cluster deleted, removing credentials")
1059		if err := removeFromKubeconfig(kubeconfig); err != nil {
1060			warn("Cluster was deleted, but we couldn't remove it from your local kubeconfig. Try doing it manually.")
1061		}
1062	}
1063	return nil
1064}
1065
1066// RunKubernetesClusterListAssociatedResources lists a Kubernetes cluster's associated resources
1067func (s *KubernetesCommandService) RunKubernetesClusterListAssociatedResources(c *CmdConfig) error {
1068	err := ensureOneArg(c)
1069	if err != nil {
1070		return err
1071	}
1072	clusterIDorName := c.Args[0]
1073
1074	clusterID, err := clusterIDize(c, clusterIDorName)
1075	if err != nil {
1076		return err
1077	}
1078
1079	kube := c.Kubernetes()
1080	resources, err := kube.ListAssociatedResourcesForDeletion(clusterID)
1081	if err != nil {
1082		return err
1083	}
1084
1085	return displayAssociatedResources(c, resources)
1086}
1087
1088// Kubeconfig
1089
1090// RunKubernetesKubeconfigShow retrieves an existing kubernetes config and prints it.
1091func (s *KubernetesCommandService) RunKubernetesKubeconfigShow(c *CmdConfig) error {
1092	err := ensureOneArg(c)
1093	if err != nil {
1094		return err
1095	}
1096	expirySeconds, err := c.Doit.GetInt(c.NS, doctl.ArgKubeConfigExpirySeconds)
1097	if err != nil {
1098		return err
1099	}
1100
1101	kube := c.Kubernetes()
1102	clusterID, err := clusterIDize(c, c.Args[0])
1103	if err != nil {
1104		return err
1105	}
1106
1107	var kubeconfig []byte
1108	if expirySeconds > 0 {
1109		kubeconfig, err = kube.GetKubeConfigWithExpiry(clusterID, int64(expirySeconds))
1110	} else {
1111		kubeconfig, err = kube.GetKubeConfig(clusterID)
1112	}
1113	if err != nil {
1114		return err
1115	}
1116
1117	_, err = c.Out.Write(kubeconfig)
1118	return err
1119}
1120
1121func cachedExecCredentialPath(id string) string {
1122	return filepath.Join(kubeconfigCachePath(), id+".json")
1123}
1124
1125// loadCachedExecCredential attempts to load the cached exec credential from disk. Never errors
1126// Returns nil if there's no credential, if it failed to load it, or if it's expired.
1127func loadCachedExecCredential(id string) (*clientauthentication.ExecCredential, error) {
1128	path := cachedExecCredentialPath(id)
1129	f, err := os.Open(path)
1130	if err != nil {
1131		if os.IsNotExist(err) {
1132			return nil, nil
1133		}
1134
1135		return nil, err
1136	}
1137
1138	defer f.Close()
1139
1140	var execCredential *clientauthentication.ExecCredential
1141	if err := json.NewDecoder(f).Decode(&execCredential); err != nil {
1142		return nil, err
1143	}
1144
1145	if execCredential.Status == nil {
1146		// Invalid ExecCredential, remove it
1147		err = os.Remove(path)
1148		return nil, err
1149	}
1150
1151	t := execCredential.Status.ExpirationTimestamp
1152	if t.IsZero() || t.Time.Before(time.Now()) {
1153		err = os.Remove(path)
1154		return nil, err
1155	}
1156
1157	return execCredential, nil
1158}
1159
1160// cacheExecCredential caches an ExecCredential to the doctl cache directory
1161func cacheExecCredential(id string, execCredential *clientauthentication.ExecCredential) error {
1162	// Don't bother caching if there's no expiration set
1163	if execCredential.Status.ExpirationTimestamp.IsZero() {
1164		return nil
1165	}
1166
1167	cachePath := kubeconfigCachePath()
1168	if err := os.MkdirAll(cachePath, os.FileMode(0700)); err != nil {
1169		return err
1170	}
1171
1172	path := cachedExecCredentialPath(id)
1173	f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(0600))
1174	if err != nil {
1175		return err
1176	}
1177	defer f.Close()
1178
1179	return json.NewEncoder(f).Encode(execCredential)
1180}
1181
1182// RunKubernetesKubeconfigExecCredential displays the exec credential. It is for internal use only.
1183func (s *KubernetesCommandService) RunKubernetesKubeconfigExecCredential(c *CmdConfig) error {
1184	err := ensureOneArg(c)
1185	if err != nil {
1186		return err
1187	}
1188
1189	version, err := c.Doit.GetString(c.NS, doctl.ArgVersion)
1190	if err != nil {
1191		return err
1192	}
1193
1194	if version != "v1beta1" {
1195		return fmt.Errorf("Invalid version %q, expected 'v1beta1'", version)
1196	}
1197
1198	kube := c.Kubernetes()
1199
1200	clusterID := c.Args[0]
1201	execCredential, err := loadCachedExecCredential(clusterID)
1202	if err != nil && Verbose {
1203		warn("%v", err)
1204	}
1205
1206	if execCredential != nil {
1207		return json.NewEncoder(c.Out).Encode(execCredential)
1208	}
1209
1210	credentials, err := kube.GetCredentials(clusterID)
1211	if err != nil {
1212		if errResponse, ok := err.(*godo.ErrorResponse); ok {
1213			return fmt.Errorf("Failed to fetch credentials for cluster %q: %v", clusterID, errResponse.Message)
1214		}
1215		return err
1216	}
1217
1218	status := &clientauthentication.ExecCredentialStatus{
1219		ClientCertificateData: string(credentials.ClientCertificateData),
1220		ClientKeyData:         string(credentials.ClientKeyData),
1221		ExpirationTimestamp:   &metav1.Time{Time: credentials.ExpiresAt},
1222		Token:                 credentials.Token,
1223	}
1224
1225	execCredential = &clientauthentication.ExecCredential{
1226		TypeMeta: metav1.TypeMeta{
1227			Kind:       execCredentialKind,
1228			APIVersion: clientauthentication.SchemeGroupVersion.String(),
1229		},
1230		Status: status,
1231	}
1232
1233	// Don't error out when caching credentials, just print it if we're being verbose
1234	if err := cacheExecCredential(clusterID, execCredential); err != nil && Verbose {
1235		warn("%v", err)
1236	}
1237
1238	return json.NewEncoder(c.Out).Encode(execCredential)
1239}
1240
1241// RunKubernetesKubeconfigSave retrieves an existing kubernetes config and saves it to your local kubeconfig.
1242func (s *KubernetesCommandService) RunKubernetesKubeconfigSave(c *CmdConfig) error {
1243	err := ensureOneArg(c)
1244	if err != nil {
1245		return err
1246	}
1247	expirySeconds, err := c.Doit.GetInt(c.NS, doctl.ArgKubeConfigExpirySeconds)
1248	if err != nil {
1249		return err
1250	}
1251
1252	kube := c.Kubernetes()
1253	clusterID, err := clusterIDize(c, c.Args[0])
1254	if err != nil {
1255		return err
1256	}
1257
1258	remoteKubeconfig, err := s.KubeconfigProvider.Remote(kube, clusterID, expirySeconds)
1259	if err != nil {
1260		return err
1261	}
1262
1263	setCurrentContext, err := c.Doit.GetBool(c.NS, doctl.ArgSetCurrentContext)
1264	if err != nil {
1265		return err
1266	}
1267
1268	path := cachedExecCredentialPath(clusterID)
1269	_, err = os.Stat(path)
1270	if err == nil {
1271		os.Remove(path)
1272	}
1273
1274	return s.writeOrAddToKubeconfig(clusterID, remoteKubeconfig, setCurrentContext, expirySeconds)
1275}
1276
1277// RunKubernetesKubeconfigRemove retrieves an existing kubernetes config and removes it from your local kubeconfig.
1278func (s *KubernetesCommandService) RunKubernetesKubeconfigRemove(c *CmdConfig) error {
1279	err := ensureOneArg(c)
1280	if err != nil {
1281		return err
1282	}
1283	kube := c.Kubernetes()
1284	clusterID, err := clusterIDize(c, c.Args[0])
1285	if err != nil {
1286		return err
1287	}
1288	kubeconfig, err := kube.GetKubeConfig(clusterID)
1289	if err != nil {
1290		return err
1291	}
1292
1293	return removeFromKubeconfig(kubeconfig)
1294}
1295
1296// Node Pools
1297
1298// RunKubernetesNodePoolGet retrieves an existing cluster node pool by its identifier.
1299func (s *KubernetesCommandService) RunKubernetesNodePoolGet(c *CmdConfig) error {
1300	if len(c.Args) != 2 {
1301		return doctl.NewMissingArgsErr(c.NS)
1302	}
1303	clusterID, err := clusterIDize(c, c.Args[0])
1304	if err != nil {
1305		return err
1306	}
1307	nodePool, err := poolByIDorName(c.Kubernetes(), clusterID, c.Args[1])
1308	if err != nil {
1309		return err
1310	}
1311	return displayNodePools(c, *nodePool)
1312}
1313
1314// RunKubernetesNodePoolList lists cluster node pool.
1315func (s *KubernetesCommandService) RunKubernetesNodePoolList(c *CmdConfig) error {
1316	err := ensureOneArg(c)
1317	if err != nil {
1318		return err
1319	}
1320	clusterID, err := clusterIDize(c, c.Args[0])
1321	if err != nil {
1322		return err
1323	}
1324	kube := c.Kubernetes()
1325	list, err := kube.ListNodePools(clusterID)
1326	if err != nil {
1327		return err
1328	}
1329
1330	return displayNodePools(c, list...)
1331}
1332
1333// RunKubernetesNodePoolCreate creates a new cluster node pool with a given configuration.
1334func (s *KubernetesCommandService) RunKubernetesNodePoolCreate(c *CmdConfig) error {
1335	err := ensureOneArg(c)
1336	if err != nil {
1337		return err
1338	}
1339	clusterID, err := clusterIDize(c, c.Args[0])
1340	if err != nil {
1341		return err
1342	}
1343
1344	r := new(godo.KubernetesNodePoolCreateRequest)
1345	if err := buildNodePoolCreateRequestFromArgs(c, r); err != nil {
1346		return err
1347	}
1348
1349	kube := c.Kubernetes()
1350	nodePool, err := kube.CreateNodePool(clusterID, r)
1351	if err != nil {
1352		return err
1353	}
1354
1355	return displayNodePools(c, *nodePool)
1356}
1357
1358// RunKubernetesNodePoolUpdate updates an existing cluster node pool with new properties.
1359func (s *KubernetesCommandService) RunKubernetesNodePoolUpdate(c *CmdConfig) error {
1360	if len(c.Args) != 2 {
1361		return doctl.NewMissingArgsErr(c.NS)
1362	}
1363	clusterID, err := clusterIDize(c, c.Args[0])
1364	if err != nil {
1365		return err
1366	}
1367	poolID, err := poolIDize(c.Kubernetes(), clusterID, c.Args[1])
1368	if err != nil {
1369		return err
1370	}
1371
1372	r := new(godo.KubernetesNodePoolUpdateRequest)
1373	if err := buildNodePoolUpdateRequestFromArgs(c, r); err != nil {
1374		return err
1375	}
1376
1377	kube := c.Kubernetes()
1378	nodePool, err := kube.UpdateNodePool(clusterID, poolID, r)
1379	if err != nil {
1380		return err
1381	}
1382
1383	return displayNodePools(c, *nodePool)
1384}
1385
1386// RunKubernetesNodePoolRecycle DEPRECATED: will be removed in v2.0, please use delete-node or replace-node
1387func (s *KubernetesCommandService) RunKubernetesNodePoolRecycle(c *CmdConfig) error {
1388	if len(c.Args) != 2 {
1389		return doctl.NewMissingArgsErr(c.NS)
1390	}
1391	clusterID, err := clusterIDize(c, c.Args[0])
1392	if err != nil {
1393		return err
1394	}
1395	poolID, err := poolIDize(c.Kubernetes(), clusterID, c.Args[1])
1396	if err != nil {
1397		return err
1398	}
1399
1400	r := new(godo.KubernetesNodePoolRecycleNodesRequest)
1401	if err := buildNodePoolRecycleRequestFromArgs(c, clusterID, poolID, r); err != nil {
1402		return err
1403	}
1404
1405	kube := c.Kubernetes()
1406	return kube.RecycleNodePoolNodes(clusterID, poolID, r)
1407}
1408
1409// RunKubernetesNodePoolDelete deletes a Kubernetes node pool
1410func (s *KubernetesCommandService) RunKubernetesNodePoolDelete(c *CmdConfig) error {
1411	if len(c.Args) != 2 {
1412		return doctl.NewMissingArgsErr(c.NS)
1413	}
1414	clusterID, err := clusterIDize(c, c.Args[0])
1415	if err != nil {
1416		return err
1417	}
1418	poolID, err := poolIDize(c.Kubernetes(), clusterID, c.Args[1])
1419	if err != nil {
1420		return err
1421	}
1422
1423	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
1424	if err != nil {
1425		return err
1426	}
1427	if force || AskForConfirmDelete("Kubernetes node pool", 1) == nil {
1428		kube := c.Kubernetes()
1429		if err := kube.DeleteNodePool(clusterID, poolID); err != nil {
1430			return err
1431		}
1432	} else {
1433		return errOperationAborted
1434	}
1435	return nil
1436}
1437
1438// RunKubernetesNodeDelete deletes a Kubernetes Node
1439func (s *KubernetesCommandService) RunKubernetesNodeDelete(c *CmdConfig) error {
1440	return kubernetesNodeDelete(false, c)
1441}
1442
1443// RunKubernetesNodeReplace replaces a Kubernetes Node
1444func (s *KubernetesCommandService) RunKubernetesNodeReplace(c *CmdConfig) error {
1445	return kubernetesNodeDelete(true, c)
1446}
1447
1448func kubernetesNodeDelete(replace bool, c *CmdConfig) error {
1449	if len(c.Args) != 3 {
1450		return doctl.NewMissingArgsErr(c.NS)
1451	}
1452	clusterID, err := clusterIDize(c, c.Args[0])
1453	if err != nil {
1454		return err
1455	}
1456	poolID, err := poolIDize(c.Kubernetes(), clusterID, c.Args[1])
1457	if err != nil {
1458		return err
1459	}
1460	nodeID := c.Args[2]
1461
1462	force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
1463	if err != nil {
1464		return err
1465	}
1466
1467	msg := "delete this Kubernetes node?"
1468	if replace {
1469		msg = "replace this Kubernetes node?"
1470	}
1471
1472	if !(force || AskForConfirm(msg) == nil) {
1473		return errOperationAborted
1474	}
1475
1476	skipDrain, err := c.Doit.GetBool(c.NS, "skip-drain")
1477	if err != nil {
1478		return err
1479	}
1480
1481	kube := c.Kubernetes()
1482	return kube.DeleteNode(clusterID, poolID, nodeID, &godo.KubernetesNodeDeleteRequest{
1483		Replace:   replace,
1484		SkipDrain: skipDrain,
1485	})
1486}
1487
1488// RunKubeOptionsListVersion lists valid versions for kubernetes clusters.
1489func (s *KubernetesCommandService) RunKubeOptionsListVersion(c *CmdConfig) error {
1490	kube := c.Kubernetes()
1491	versions, err := kube.GetVersions()
1492	if err != nil {
1493		return err
1494	}
1495	item := &displayers.KubernetesVersions{KubernetesVersions: versions}
1496	return c.Display(item)
1497}
1498
1499// RunKubeOptionsListRegion lists valid regions for kubernetes clusters.
1500func (s *KubernetesCommandService) RunKubeOptionsListRegion(c *CmdConfig) error {
1501	kube := c.Kubernetes()
1502	regions, err := kube.GetRegions()
1503	if err != nil {
1504		return err
1505	}
1506	item := &displayers.KubernetesRegions{KubernetesRegions: regions}
1507	return c.Display(item)
1508}
1509
1510// RunKubeOptionsListNodeSizes lists valid node sizes for kubernetes clusters.
1511func (s *KubernetesCommandService) RunKubeOptionsListNodeSizes(c *CmdConfig) error {
1512	kube := c.Kubernetes()
1513	sizes, err := kube.GetNodeSizes()
1514	if err != nil {
1515		return err
1516	}
1517	item := &displayers.KubernetesNodeSizes{KubernetesNodeSizes: sizes}
1518	return c.Display(item)
1519}
1520
1521func (s *KubernetesCommandService) RunKubernetesRegistryAdd(c *CmdConfig) error {
1522	if len(c.Args) < 1 {
1523		return doctl.NewMissingArgsErr(c.NS)
1524	}
1525	clusterUUIDs := make([]string, 0, len(c.Args))
1526	for _, arg := range c.Args {
1527		clusterID, err := clusterIDize(c, arg)
1528		if err != nil {
1529			return err
1530		}
1531		clusterUUIDs = append(clusterUUIDs, clusterID)
1532	}
1533	r := new(godo.KubernetesClusterRegistryRequest)
1534	r.ClusterUUIDs = clusterUUIDs
1535
1536	kube := c.Kubernetes()
1537	return kube.AddRegistry(r)
1538}
1539
1540func (s *KubernetesCommandService) RunKubernetesRegistryRemove(c *CmdConfig) error {
1541	if len(c.Args) < 1 {
1542		return doctl.NewMissingArgsErr(c.NS)
1543	}
1544	clusterUUIDs := make([]string, 0, len(c.Args))
1545	for _, arg := range c.Args {
1546		clusterID, err := clusterIDize(c, arg)
1547		if err != nil {
1548			return err
1549		}
1550		clusterUUIDs = append(clusterUUIDs, clusterID)
1551	}
1552	r := new(godo.KubernetesClusterRegistryRequest)
1553	r.ClusterUUIDs = clusterUUIDs
1554
1555	kube := c.Kubernetes()
1556	return kube.RemoveRegistry(r)
1557}
1558
1559func buildClusterCreateRequestFromArgs(c *CmdConfig, r *godo.KubernetesClusterCreateRequest, defaultNodeSize string, defaultNodeCount int) error {
1560	region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug)
1561	if err != nil {
1562		return err
1563	}
1564	r.RegionSlug = region
1565
1566	vpcUUID, err := c.Doit.GetString(c.NS, doctl.ArgClusterVPCUUID)
1567	if err != nil {
1568		return err
1569	}
1570	// empty "" is fine, the default region VPC will be resolved
1571	r.VPCUUID = vpcUUID
1572
1573	version, err := getVersionOrLatest(c)
1574	if err != nil {
1575		return err
1576	}
1577	r.VersionSlug = version
1578
1579	autoUpgrade, err := c.Doit.GetBool(c.NS, doctl.ArgAutoUpgrade)
1580	if err != nil {
1581		return err
1582	}
1583	r.AutoUpgrade = autoUpgrade
1584
1585	surgeUpgrade, err := c.Doit.GetBool(c.NS, doctl.ArgSurgeUpgrade)
1586	if err != nil {
1587		return err
1588	}
1589	r.SurgeUpgrade = surgeUpgrade
1590
1591	ha, err := c.Doit.GetBool(c.NS, doctl.ArgHA)
1592	if err != nil {
1593		return err
1594	}
1595	r.HA = ha
1596
1597	tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgTag)
1598	if err != nil {
1599		return err
1600	}
1601	r.Tags = tags
1602
1603	maintenancePolicy, err := parseMaintenancePolicy(c)
1604	if err != nil {
1605		return err
1606	}
1607	r.MaintenancePolicy = maintenancePolicy
1608
1609	// node pools
1610	nodePoolSpecs, err := c.Doit.GetStringSlice(c.NS, doctl.ArgClusterNodePool)
1611	if err != nil {
1612		return err
1613	}
1614
1615	if len(nodePoolSpecs) == 0 {
1616		nodePoolSize, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug)
1617		if err != nil {
1618			return err
1619		}
1620
1621		nodePoolCount, err := c.Doit.GetInt(c.NS, doctl.ArgNodePoolCount)
1622		if err != nil {
1623			return err
1624		}
1625
1626		nodePoolName := r.Name + "-default-pool"
1627		r.NodePools = []*godo.KubernetesNodePoolCreateRequest{{
1628			Name:  nodePoolName,
1629			Size:  nodePoolSize,
1630			Count: nodePoolCount,
1631		}}
1632
1633		return nil
1634	}
1635
1636	// multiple node pools
1637	if c.Doit.IsSet(doctl.ArgSizeSlug) || c.Doit.IsSet(doctl.ArgNodePoolCount) {
1638		return fmt.Errorf("Flags %q and %q cannot be provided when %q is present", doctl.ArgSizeSlug, doctl.ArgNodePoolCount, doctl.ArgClusterNodePool)
1639	}
1640
1641	nodePools, err := buildNodePoolCreateRequestsFromArgs(c, nodePoolSpecs, r.Name, defaultNodeSize, defaultNodeCount)
1642	if err != nil {
1643		return err
1644	}
1645	r.NodePools = nodePools
1646
1647	return nil
1648}
1649
1650func buildClusterUpdateRequestFromArgs(c *CmdConfig, r *godo.KubernetesClusterUpdateRequest) error {
1651	name, err := c.Doit.GetString(c.NS, doctl.ArgClusterName)
1652	if err != nil {
1653		return err
1654	}
1655	r.Name = name
1656
1657	tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgTag)
1658	if err != nil {
1659		return err
1660	}
1661	r.Tags = tags
1662
1663	maintenancePolicy, err := parseMaintenancePolicy(c)
1664	if err != nil {
1665		return err
1666	}
1667	r.MaintenancePolicy = maintenancePolicy
1668
1669	autoUpgrade, err := c.Doit.GetBoolPtr(c.NS, doctl.ArgAutoUpgrade)
1670	if err != nil {
1671		return err
1672	}
1673	r.AutoUpgrade = autoUpgrade
1674
1675	surgeUpgrade, err := c.Doit.GetBool(c.NS, doctl.ArgSurgeUpgrade)
1676	if err != nil {
1677		return err
1678	}
1679	r.SurgeUpgrade = surgeUpgrade
1680
1681	return nil
1682}
1683
1684func buildNodePoolRecycleRequestFromArgs(c *CmdConfig, clusterID, poolID string, r *godo.KubernetesNodePoolRecycleNodesRequest) error {
1685	nodeIDorNames, err := c.Doit.GetStringSlice(c.NS, doctl.ArgNodePoolNodeIDs)
1686	if err != nil {
1687		return err
1688	}
1689	allUUIDs := true
1690	for _, node := range nodeIDorNames {
1691		if !looksLikeUUID(node) {
1692			allUUIDs = false
1693		}
1694	}
1695	if allUUIDs {
1696		r.Nodes = nodeIDorNames
1697	} else {
1698		// at least some args weren't UUIDs, so assume that they're all names
1699		nodes, err := nodesByNames(c.Kubernetes(), clusterID, poolID, nodeIDorNames)
1700		if err != nil {
1701			return err
1702		}
1703		for _, node := range nodes {
1704			r.Nodes = append(r.Nodes, node.ID)
1705		}
1706	}
1707	return nil
1708}
1709
1710func buildNodePoolCreateRequestsFromArgs(c *CmdConfig, nodePools []string, clusterName, defaultSize string, defaultCount int) ([]*godo.KubernetesNodePoolCreateRequest, error) {
1711	out := make([]*godo.KubernetesNodePoolCreateRequest, 0, len(nodePools))
1712	for i, nodePoolString := range nodePools {
1713		defaultName := fmt.Sprintf("%s-pool-%d", clusterName, i+1)
1714		poolCreateReq, err := parseNodePoolString(nodePoolString, defaultName, defaultSize, defaultCount)
1715		if err != nil {
1716			return nil, fmt.Errorf("Invalid node pool arguments for flag %d: %v", i, err)
1717		}
1718		out = append(out, poolCreateReq)
1719	}
1720	return out, nil
1721}
1722
1723func parseNodePoolString(nodePool, defaultName, defaultSize string, defaultCount int) (*godo.KubernetesNodePoolCreateRequest, error) {
1724	const (
1725		argSeparator = ";"
1726		kvSeparator  = "="
1727	)
1728	out := &godo.KubernetesNodePoolCreateRequest{
1729		Name:   defaultName,
1730		Size:   defaultSize,
1731		Count:  defaultCount,
1732		Labels: map[string]string{},
1733		Taints: []godo.Taint{},
1734	}
1735	trimmedPool := strings.TrimSuffix(nodePool, argSeparator)
1736	for _, arg := range strings.Split(trimmedPool, argSeparator) {
1737		kvs := strings.SplitN(arg, kvSeparator, 2)
1738		if len(kvs) < 2 {
1739			return nil, fmt.Errorf("A node pool string argument must be of the form `key=value`. Provided KVs: %v", kvs)
1740		}
1741		key := kvs[0]
1742		value := kvs[1]
1743		switch key {
1744		case "name":
1745			out.Name = value
1746		case "size":
1747			out.Size = value
1748		case "count":
1749			count, err := strconv.ParseInt(value, 10, 64)
1750			if err != nil {
1751				return nil, errors.New("Node pool count must be a valid integer.")
1752			}
1753			out.Count = int(count)
1754		case "tag":
1755			out.Tags = append(out.Tags, value)
1756		case "label":
1757			labelParts := strings.SplitN(value, "=", 2)
1758			if len(labelParts) < 2 {
1759				return nil, fmt.Errorf("a node pool label component must be of the form `label-key=label-value`, got %q", value)
1760			}
1761			labelKey := labelParts[0]
1762			labelValue := labelParts[1]
1763			out.Labels[labelKey] = labelValue
1764		case "taint":
1765			taint, err := parseTaint(value)
1766			if err != nil {
1767				return nil, fmt.Errorf("failed to parse taint: %s", err)
1768			}
1769			out.Taints = append(out.Taints, taint)
1770		case "auto-scale":
1771			autoScale, err := strconv.ParseBool(value)
1772			if err != nil {
1773				return nil, errors.New("Node pool auto-scale value must be a valid boolean.")
1774			}
1775			out.AutoScale = autoScale
1776		case "min-nodes":
1777			minNodes, err := strconv.ParseInt(value, 10, 64)
1778			if err != nil {
1779				return nil, errors.New("Node pool min-nodes must be a valid integer.")
1780			}
1781			out.MinNodes = int(minNodes)
1782		case "max-nodes":
1783			maxNodes, err := strconv.ParseInt(value, 10, 64)
1784			if err != nil {
1785				return nil, errors.New("Node pool max-nodes must be a valid integer.")
1786			}
1787			out.MaxNodes = int(maxNodes)
1788		default:
1789			return nil, fmt.Errorf("Unsupported node pool argument %q", key)
1790		}
1791	}
1792	return out, nil
1793}
1794
1795func buildNodePoolCreateRequestFromArgs(c *CmdConfig, r *godo.KubernetesNodePoolCreateRequest) error {
1796	name, err := c.Doit.GetString(c.NS, doctl.ArgNodePoolName)
1797	if err != nil {
1798		return err
1799	}
1800	r.Name = name
1801
1802	size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug)
1803	if err != nil {
1804		return err
1805	}
1806	r.Size = size
1807
1808	count, err := c.Doit.GetIntPtr(c.NS, doctl.ArgNodePoolCount)
1809	if err != nil {
1810		return err
1811	}
1812	if count == nil {
1813		count = intPtr(0)
1814	}
1815	r.Count = *count
1816
1817	tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgTag)
1818	if err != nil {
1819		return err
1820	}
1821	r.Tags = tags
1822
1823	labels, err := c.Doit.GetStringMapString(c.NS, doctl.ArgKubernetesLabel)
1824	if err != nil {
1825		return err
1826	}
1827	r.Labels = labels
1828
1829	rawTaints, err := c.Doit.GetStringSlice(c.NS, doctl.ArgKubernetesTaint)
1830	if err != nil {
1831		return err
1832	}
1833	taints, err := parseTaints(rawTaints)
1834	if err != nil {
1835		return fmt.Errorf("failed to parse taints: %s", err)
1836	}
1837	r.Taints = taints
1838
1839	autoScale, err := c.Doit.GetBool(c.NS, doctl.ArgNodePoolAutoScale)
1840	if err != nil {
1841		return err
1842	}
1843	r.AutoScale = autoScale
1844
1845	minNodes, err := c.Doit.GetInt(c.NS, doctl.ArgNodePoolMinNodes)
1846	if err != nil {
1847		return err
1848	}
1849	r.MinNodes = minNodes
1850
1851	maxNodes, err := c.Doit.GetInt(c.NS, doctl.ArgNodePoolMaxNodes)
1852	if err != nil {
1853		return err
1854	}
1855	r.MaxNodes = maxNodes
1856
1857	return nil
1858}
1859
1860func buildNodePoolUpdateRequestFromArgs(c *CmdConfig, r *godo.KubernetesNodePoolUpdateRequest) error {
1861	name, err := c.Doit.GetString(c.NS, doctl.ArgNodePoolName)
1862	if err != nil {
1863		return err
1864	}
1865	r.Name = name
1866
1867	count, err := c.Doit.GetIntPtr(c.NS, doctl.ArgNodePoolCount)
1868	if err != nil {
1869		return err
1870	}
1871	r.Count = count
1872
1873	tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgTag)
1874	if err != nil {
1875		return err
1876	}
1877	r.Tags = tags
1878
1879	labels, err := c.Doit.GetStringMapString(c.NS, doctl.ArgKubernetesLabel)
1880	if err != nil {
1881		return err
1882	}
1883	r.Labels = labels
1884
1885	// Check if the taints flag is set so that we can distinguish between not
1886	// setting any taints, setting the empty taint (which equals clearing all
1887	// taints), and setting one or more non-empty taints.
1888	if c.Doit.IsSet(doctl.ArgKubernetesTaint) {
1889		rawTaints, err := c.Doit.GetStringSlice(c.NS, doctl.ArgKubernetesTaint)
1890		if err != nil {
1891			return err
1892		}
1893		taints, err := parseTaints(rawTaints)
1894		if err != nil {
1895			return fmt.Errorf("failed to parse taints: %s", err)
1896		}
1897		r.Taints = &taints
1898	}
1899
1900	autoScale, err := c.Doit.GetBoolPtr(c.NS, doctl.ArgNodePoolAutoScale)
1901	if err != nil {
1902		return err
1903	}
1904	r.AutoScale = autoScale
1905
1906	minNodes, err := c.Doit.GetIntPtr(c.NS, doctl.ArgNodePoolMinNodes)
1907	if err != nil {
1908		return err
1909	}
1910	r.MinNodes = minNodes
1911
1912	maxNodes, err := c.Doit.GetIntPtr(c.NS, doctl.ArgNodePoolMaxNodes)
1913	if err != nil {
1914		return err
1915	}
1916	r.MaxNodes = maxNodes
1917
1918	return nil
1919}
1920
1921func (s *KubernetesCommandService) writeOrAddToKubeconfig(clusterID string, remoteKubeconfig *clientcmdapi.Config, setCurrentContext bool, expirySeconds int) error {
1922	localKubeconfig, err := s.KubeconfigProvider.Local()
1923	if err != nil {
1924		return err
1925	}
1926
1927	kubectlDefaults := s.KubeconfigProvider.ConfigPath()
1928	notice("Adding cluster credentials to kubeconfig file found in %q", kubectlDefaults)
1929	if err := mergeKubeconfig(clusterID, remoteKubeconfig, localKubeconfig, setCurrentContext, expirySeconds); err != nil {
1930		return fmt.Errorf("Couldn't use the kubeconfig info received, %v", err)
1931	}
1932
1933	return s.KubeconfigProvider.Write(localKubeconfig)
1934}
1935
1936func removeFromKubeconfig(kubeconfig []byte) error {
1937	remote, err := clientcmd.Load(kubeconfig)
1938	if err != nil {
1939		return err
1940	}
1941	kubectlDefaults := clientcmd.NewDefaultPathOptions()
1942	currentConfig, err := kubectlDefaults.GetStartingConfig()
1943	if err != nil {
1944		return err
1945	}
1946	notice("Removing cluster credentials from kubeconfig file found in %q", kubectlDefaults.GlobalFile)
1947	if err := removeKubeconfig(remote, currentConfig); err != nil {
1948		return fmt.Errorf("Couldn't use the kubeconfig info received, %v", err)
1949	}
1950	return clientcmd.ModifyConfig(kubectlDefaults, *currentConfig, false)
1951}
1952
1953// mergeKubeconfig merges a remote cluster's config file with a local config file,
1954// assuming that the current context in the remote config file points to the
1955// cluster details to add to the local config.
1956func mergeKubeconfig(clusterID string, remote, local *clientcmdapi.Config, setCurrentContext bool, expirySeconds int) error {
1957	remoteCtx, ok := remote.Contexts[remote.CurrentContext]
1958	if !ok {
1959		// this is a bug in the backend, we received incomplete/non-sensical data
1960		return fmt.Errorf("The remote config has no context entry named %q. This is a bug, please open a ticket with DigitalOcean.",
1961			remote.CurrentContext,
1962		)
1963	}
1964	remoteCluster, ok := remote.Clusters[remoteCtx.Cluster]
1965	if !ok {
1966		// this is a bug in the backend, we received incomplete/non-sensical data
1967		return fmt.Errorf("The remote config has no cluster entry named %q. This is a bug, please open a ticket with DigitalOcean.",
1968			remoteCtx.Cluster,
1969		)
1970	}
1971
1972	local.Contexts[remote.CurrentContext] = remoteCtx
1973	local.Clusters[remoteCtx.Cluster] = remoteCluster
1974
1975	if setCurrentContext {
1976		notice("Setting current-context to %s", remote.CurrentContext)
1977		local.CurrentContext = remote.CurrentContext
1978	}
1979
1980	switch {
1981	case expirySeconds > 0:
1982		// When expirySeconds is passed, token based auth should be used as
1983		// credentials should expire and not be renewed automatically
1984		local.AuthInfos[remoteCtx.AuthInfo] = &clientcmdapi.AuthInfo{
1985			Token: remote.AuthInfos[remoteCtx.AuthInfo].Token,
1986		}
1987	default:
1988		// Configure kubectl to call doctl to renew credentials automatically
1989		local.AuthInfos[remoteCtx.AuthInfo] = &clientcmdapi.AuthInfo{
1990			Exec: &clientcmdapi.ExecConfig{
1991				APIVersion: clientauthentication.SchemeGroupVersion.String(),
1992				Command:    doctl.CommandName(),
1993				Args: []string{
1994					"kubernetes",
1995					"cluster",
1996					"kubeconfig",
1997					"exec-credential",
1998					"--version=v1beta1",
1999					"--context=" + getCurrentAuthContextFn(),
2000					clusterID,
2001				},
2002			},
2003		}
2004	}
2005
2006	return nil
2007}
2008
2009// removeKubeconfig removes a remote cluster's config file from a local config file,
2010// assuming that the current context in the remote config file points to the
2011// cluster details to remove from the local config.
2012func removeKubeconfig(remote, local *clientcmdapi.Config) error {
2013	remoteCtx, ok := remote.Contexts[remote.CurrentContext]
2014	if !ok {
2015		// this is a bug in the backend, we received incomplete/non-sensical data
2016		return fmt.Errorf("The remote config has no context entry named %q. This is a bug, please open a ticket with DigitalOcean.",
2017			remote.CurrentContext,
2018		)
2019	}
2020
2021	delete(local.Contexts, remote.CurrentContext)
2022	delete(local.Clusters, remoteCtx.Cluster)
2023	delete(local.AuthInfos, remoteCtx.AuthInfo)
2024	if local.CurrentContext == remote.CurrentContext {
2025		local.CurrentContext = ""
2026		notice("The removed cluster was set as the current context in kubectl. Run `kubectl config get-contexts` to see a list of other contexts you can use, and `kubectl config set-context` to specify a new one.")
2027	}
2028	return nil
2029}
2030
2031// waitForClusterRunning waits for a cluster to be running.
2032func waitForClusterRunning(kube do.KubernetesService, clusterID string) (*do.KubernetesCluster, error) {
2033	failCount := 0
2034	printNewLineSet := false
2035	for i := 0; ; i++ {
2036		if i != 0 {
2037			fmt.Fprint(os.Stderr, ".")
2038			if !printNewLineSet {
2039				printNewLineSet = true
2040				defer fmt.Fprintln(os.Stderr)
2041			}
2042		}
2043		cluster, err := kube.Get(clusterID)
2044		if err == nil {
2045			failCount = 0
2046		} else {
2047			// Allow for transient API failures
2048			failCount++
2049			if failCount >= maxAPIFailures {
2050				return nil, err
2051			}
2052		}
2053
2054		if cluster == nil || cluster.Status == nil {
2055			time.Sleep(1 * time.Second)
2056			continue
2057		}
2058		switch cluster.Status.State {
2059		case godo.KubernetesClusterStatusRunning:
2060			return cluster, nil
2061		case godo.KubernetesClusterStatusProvisioning:
2062			time.Sleep(5 * time.Second)
2063		default:
2064			return cluster, fmt.Errorf("Unknown status: [%s]", cluster.Status.State)
2065		}
2066	}
2067}
2068
2069func displayClusters(c *CmdConfig, short bool, clusters ...do.KubernetesCluster) error {
2070	item := &displayers.KubernetesClusters{KubernetesClusters: do.KubernetesClusters(clusters), Short: short}
2071	return c.Display(item)
2072}
2073
2074func displayNodePools(c *CmdConfig, nodePools ...do.KubernetesNodePool) error {
2075	item := &displayers.KubernetesNodePools{KubernetesNodePools: do.KubernetesNodePools(nodePools)}
2076	return c.Display(item)
2077}
2078
2079func displayAssociatedResources(c *CmdConfig, ar *do.KubernetesAssociatedResources) error {
2080	item := &displayers.KubernetesAssociatedResources{KubernetesAssociatedResources: ar}
2081	return c.Display(item)
2082}
2083
2084// clusterByIDorName attempts to find a cluster by ID or by name if the argument isn't an ID. If multiple
2085// clusters have the same name, then an error with the cluster IDs matching this name is returned.
2086func clusterByIDorName(kube do.KubernetesService, idOrName string) (*do.KubernetesCluster, error) {
2087	if looksLikeUUID(idOrName) {
2088		clusterID := idOrName
2089		return kube.Get(clusterID)
2090	}
2091	clusters, err := kube.List()
2092	if err != nil {
2093		return nil, err
2094	}
2095	var out []*do.KubernetesCluster
2096	for _, c := range clusters {
2097		c1 := c
2098		if c.Name == idOrName {
2099			out = append(out, &c1)
2100		}
2101	}
2102	switch {
2103	case len(out) == 0:
2104		return nil, errNoClusterByName(idOrName)
2105	case len(out) > 1:
2106		var ids []string
2107		for _, c := range out {
2108			ids = append(ids, c.ID)
2109		}
2110		return nil, errAmbiguousClusterName(idOrName, ids)
2111	default:
2112		if len(out) != 1 {
2113			panic("The default case should always have len(out) == 1.")
2114		}
2115		return out[0], nil
2116	}
2117}
2118
2119// clusterIDize attempts to make a cluster ID/name string be a cluster ID.
2120// use this as opposed to `clusterByIDorName` if you just care about getting
2121// a cluster ID and don't need the cluster object itself
2122func clusterIDize(c *CmdConfig, idOrName string) (string, error) {
2123	return iDize(c, idOrName, "cluster", "")
2124}
2125
2126// iDize attempts to make a resource ID/name string be a resource ID.
2127// use this if you just care about getting a resource ID and don't need the object itself
2128func iDize(c *CmdConfig, resourceIDOrName string, resType string, regionSlug string) (string, error) {
2129	if looksLikeUUID(resourceIDOrName) {
2130		return resourceIDOrName, nil
2131	}
2132	var ids []string
2133
2134	switch resType {
2135	case "volume":
2136		volumes, err := c.Volumes().List()
2137		if err != nil {
2138			return "", err
2139		}
2140
2141		for _, v := range volumes {
2142			if v.Name == resourceIDOrName && v.Region.Slug == regionSlug {
2143				id := v.ID
2144				ids = append(ids, id)
2145			}
2146		}
2147	case "volume_snapshot":
2148		volSnapshots, err := c.Snapshots().ListVolume()
2149		if err != nil {
2150			return "", err
2151		}
2152
2153		for _, v := range volSnapshots {
2154			if v.Name == resourceIDOrName && contains(v.Regions, regionSlug) {
2155				id := v.ID
2156				ids = append(ids, id)
2157			}
2158		}
2159	case "load_balancer":
2160		loadBalancers, err := c.LoadBalancers().List()
2161		if err != nil {
2162			return "", err
2163		}
2164		for _, l := range loadBalancers {
2165			if l.Name == resourceIDOrName {
2166				id := l.ID
2167				ids = append(ids, id)
2168			}
2169		}
2170	case "cluster":
2171		clusters, err := c.Kubernetes().List()
2172		if err != nil {
2173			return "", err
2174		}
2175		for _, c := range clusters {
2176			if c.Name == resourceIDOrName {
2177				id := c.ID
2178				ids = append(ids, id)
2179			}
2180		}
2181	}
2182
2183	switch {
2184	case len(ids) == 0:
2185		return "", fmt.Errorf("no %s goes by the name %q", resType, resourceIDOrName)
2186	case len(ids) > 1:
2187		return "", fmt.Errorf("many %ss go by the name %q, they have the following IDs: %v", resType, resourceIDOrName, ids)
2188	default:
2189		if len(ids) != 1 {
2190			panic("The default case should always have len(ids) == 1.")
2191		}
2192		return ids[0], nil
2193	}
2194}
2195
2196func contains(regions []string, region string) bool {
2197	for _, r := range regions {
2198		if r == region {
2199			return true
2200		}
2201	}
2202	return false
2203}
2204
2205// poolByIDorName attempts to find a pool by ID or by name if the argument isn't an ID. If multiple
2206// pools have the same name, then an error with the pool IDs matching this name is returned.
2207func poolByIDorName(kube do.KubernetesService, clusterID, idOrName string) (*do.KubernetesNodePool, error) {
2208	if looksLikeUUID(idOrName) {
2209		poolID := idOrName
2210		return kube.GetNodePool(clusterID, poolID)
2211	}
2212	nodePools, err := kube.ListNodePools(clusterID)
2213	if err != nil {
2214		return nil, err
2215	}
2216	var out []*do.KubernetesNodePool
2217	for _, c := range nodePools {
2218		c1 := c
2219		if c.Name == idOrName {
2220			out = append(out, &c1)
2221		}
2222	}
2223	switch {
2224	case len(out) == 0:
2225		return nil, errNoPoolByName(idOrName)
2226	case len(out) > 1:
2227		var ids []string
2228		for _, c := range out {
2229			ids = append(ids, c.ID)
2230		}
2231		return nil, errAmbiguousPoolName(idOrName, ids)
2232	default:
2233		if len(out) != 1 {
2234			panic("The default case should always have len(out) == 1.")
2235		}
2236		return out[0], nil
2237	}
2238}
2239
2240// poolIDize attempts to make a node pool ID/name string be a node pool ID.
2241// use this as opposed to `poolByIDorName` if you just care about getting
2242// a node pool ID and don't need the node pool object itself
2243func poolIDize(kube do.KubernetesService, clusterID, idOrName string) (string, error) {
2244	if looksLikeUUID(idOrName) {
2245		return idOrName, nil
2246	}
2247	pools, err := kube.ListNodePools(clusterID)
2248	if err != nil {
2249		return "", err
2250	}
2251	var ids []string
2252	for _, c := range pools {
2253		if c.Name == idOrName {
2254			ids = append(ids, c.ID)
2255		}
2256	}
2257	switch {
2258	case len(ids) == 0:
2259		return "", errNoPoolByName(idOrName)
2260	case len(ids) > 1:
2261		return "", errAmbiguousPoolName(idOrName, ids)
2262	default:
2263		if len(ids) != 1 {
2264			panic("The default case should always have len(ids) == 1.")
2265		}
2266		return ids[0], nil
2267	}
2268}
2269
2270// nodesByNames attempts to find nodes by names. If multiple nodes have the same name,
2271// then an error with the node IDs matching this name is returned.
2272func nodesByNames(kube do.KubernetesService, clusterID, poolID string, nodeNames []string) ([]*godo.KubernetesNode, error) {
2273	nodePool, err := kube.GetNodePool(clusterID, poolID)
2274	if err != nil {
2275		return nil, err
2276	}
2277	out := make([]*godo.KubernetesNode, 0, len(nodeNames))
2278	for _, name := range nodeNames {
2279		node, err := nodeByName(name, nodePool.Nodes)
2280		if err != nil {
2281			return nil, err
2282		}
2283		out = append(out, node)
2284	}
2285	return out, nil
2286}
2287
2288func nodeByName(name string, nodes []*godo.KubernetesNode) (*godo.KubernetesNode, error) {
2289	var out []*godo.KubernetesNode
2290	for _, n := range nodes {
2291		n1 := n
2292		if n.Name == name {
2293			out = append(out, n1)
2294		}
2295	}
2296	switch {
2297	case len(out) == 0:
2298		return nil, errNoClusterNodeByName(name)
2299	case len(out) > 1:
2300		var ids []string
2301		for _, c := range out {
2302			ids = append(ids, c.ID)
2303		}
2304		return nil, errAmbiguousClusterNodeName(name, ids)
2305	default:
2306		if len(out) != 1 {
2307			panic("The default case should always have len(out) == 1.")
2308		}
2309		return out[0], nil
2310	}
2311}
2312
2313func looksLikeUUID(str string) bool {
2314	_, err := uuid.Parse(str)
2315	if err != nil {
2316		return false
2317	}
2318
2319	// support only hyphenated UUIDs
2320	return strings.Contains(str, "-")
2321}
2322
2323func getVersionOrLatest(c *CmdConfig) (string, error) {
2324	version, err := c.Doit.GetString(c.NS, doctl.ArgClusterVersionSlug)
2325	if err != nil {
2326		return "", err
2327	}
2328	if version != "" && version != defaultKubernetesLatestVersion {
2329		return version, nil
2330	}
2331	versions, err := c.Kubernetes().GetVersions()
2332	if err != nil {
2333		return "", fmt.Errorf("No version flag provided. Unable to lookup the latest version from the API: %v", err)
2334	}
2335	if len(versions) > 0 {
2336		return versions[0].Slug, nil
2337	}
2338	releases, err := latestReleases(versions)
2339	if err != nil {
2340		return "", err
2341	}
2342	i, err := versionMaxBy(releases, func(v do.KubernetesVersion) string {
2343		return v.KubernetesVersion.KubernetesVersion
2344	})
2345	if err != nil {
2346		return "", err
2347	}
2348	return releases[i].Slug, nil
2349}
2350
2351func parseMaintenancePolicy(c *CmdConfig) (*godo.KubernetesMaintenancePolicy, error) {
2352	maintenanceWindow, err := c.Doit.GetString(c.NS, doctl.ArgMaintenanceWindow)
2353	if err != nil {
2354		return nil, err
2355	}
2356
2357	splitted := strings.SplitN(maintenanceWindow, "=", 2)
2358	if len(splitted) != 2 {
2359		return nil, fmt.Errorf("A maintenance window argument must be of the form `day=HH:MM`, got: %v", splitted)
2360	}
2361
2362	day, err := godo.KubernetesMaintenanceToDay(splitted[0])
2363	if err != nil {
2364		return nil, err
2365	}
2366
2367	return &godo.KubernetesMaintenancePolicy{
2368		StartTime: splitted[1],
2369		Day:       day,
2370	}, nil
2371}
2372
2373func latestReleases(versions []do.KubernetesVersion) ([]do.KubernetesVersion, error) {
2374	versionsByK8S := versionMapBy(versions, func(v do.KubernetesVersion) string {
2375		return v.KubernetesVersion.KubernetesVersion
2376	})
2377
2378	out := make([]do.KubernetesVersion, 0, len(versionsByK8S))
2379	for _, versions := range versionsByK8S {
2380		i, err := versionMaxBy(versions, func(v do.KubernetesVersion) string {
2381			return v.Slug
2382		})
2383		if err != nil {
2384			return nil, err
2385		}
2386		out = append(out, versions[i])
2387	}
2388	var serr error
2389	out = versionSortBy(out, func(i, j do.KubernetesVersion) bool {
2390		iv, err := semver.Parse(i.KubernetesVersion.KubernetesVersion)
2391		if err != nil {
2392			serr = err
2393			return false
2394		}
2395		jv, err := semver.Parse(j.KubernetesVersion.KubernetesVersion)
2396		if err != nil {
2397			serr = err
2398			return false
2399		}
2400		return iv.LT(jv)
2401	})
2402	return out, serr
2403}
2404
2405func versionMapBy(versions []do.KubernetesVersion, selector func(do.KubernetesVersion) string) map[string][]do.KubernetesVersion {
2406	m := make(map[string][]do.KubernetesVersion)
2407	for _, v := range versions {
2408		key := selector(v)
2409		m[key] = append(m[key], v)
2410	}
2411	return m
2412}
2413
2414func versionMaxBy(versions []do.KubernetesVersion, selector func(do.KubernetesVersion) string) (int, error) {
2415	if len(versions) == 0 {
2416		return -1, nil
2417	}
2418	if len(versions) == 1 {
2419		return 0, nil
2420	}
2421	max := 0
2422	maxSV, err := semver.Parse(selector(versions[max]))
2423	if err != nil {
2424		return max, err
2425	}
2426	// NOTE: We have to iterate over all versions here even though we know
2427	// versions[0] won't be greater than maxSV so that the index i will be a
2428	// valid index into versions rather than into versions[1:].
2429	for i, v := range versions {
2430		sv, err := semver.Parse(selector(v))
2431		if err != nil {
2432			return max, err
2433		}
2434		if sv.GT(maxSV) {
2435			max = i
2436			maxSV = sv
2437		}
2438	}
2439	return max, nil
2440}
2441
2442func versionSortBy(versions []do.KubernetesVersion, less func(i, j do.KubernetesVersion) bool) []do.KubernetesVersion {
2443	sort.Slice(versions, func(i, j int) bool { return less(versions[i], versions[j]) })
2444	return versions
2445}
2446
2447func parseTaints(rawTaints []string) ([]godo.Taint, error) {
2448	taints := make([]godo.Taint, 0, len(rawTaints))
2449	for _, rawTaint := range rawTaints {
2450		taint, err := parseTaint(rawTaint)
2451		if err != nil {
2452			return nil, err
2453		}
2454
2455		taints = append(taints, taint)
2456	}
2457
2458	return taints, nil
2459}
2460
2461func parseTaint(rawTaint string) (godo.Taint, error) {
2462	var key, value, effect string
2463
2464	parts := strings.Split(rawTaint, ":")
2465	if len(parts) != 2 {
2466		return godo.Taint{}, fmt.Errorf("taint %q does not have a single colon separator", rawTaint)
2467	}
2468
2469	keyValueParts := strings.Split(parts[0], "=")
2470	if len(keyValueParts) > 2 {
2471		return godo.Taint{}, fmt.Errorf("key/value part in taint %q must not consist of more than one equal sign", rawTaint)
2472	}
2473	key = keyValueParts[0]
2474	if len(keyValueParts) == 2 {
2475		value = keyValueParts[1]
2476	}
2477	effect = parts[1]
2478
2479	return godo.Taint{
2480		Key:    key,
2481		Value:  value,
2482		Effect: effect,
2483	}, nil
2484}
2485
2486func boolPtr(val bool) *bool {
2487	return &val
2488}
2489
2490func intPtr(val int) *int {
2491	return &val
2492}
2493