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