github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/google/resource_container_cluster.go (about) 1 package google 2 3 import ( 4 "fmt" 5 "log" 6 "net" 7 "regexp" 8 9 "github.com/hashicorp/terraform/helper/resource" 10 "github.com/hashicorp/terraform/helper/schema" 11 "google.golang.org/api/container/v1" 12 "google.golang.org/api/googleapi" 13 ) 14 15 var ( 16 instanceGroupManagerURL = regexp.MustCompile("^https://www.googleapis.com/compute/v1/projects/([a-z][a-z0-9-]{5}(?:[-a-z0-9]{0,23}[a-z0-9])?)/zones/([a-z0-9-]*)/instanceGroupManagers/([^/]*)") 17 ) 18 19 func resourceContainerCluster() *schema.Resource { 20 return &schema.Resource{ 21 Create: resourceContainerClusterCreate, 22 Read: resourceContainerClusterRead, 23 Update: resourceContainerClusterUpdate, 24 Delete: resourceContainerClusterDelete, 25 26 Schema: map[string]*schema.Schema{ 27 "master_auth": &schema.Schema{ 28 Type: schema.TypeList, 29 Required: true, 30 ForceNew: true, 31 Elem: &schema.Resource{ 32 Schema: map[string]*schema.Schema{ 33 "client_certificate": &schema.Schema{ 34 Type: schema.TypeString, 35 Computed: true, 36 }, 37 "client_key": &schema.Schema{ 38 Type: schema.TypeString, 39 Computed: true, 40 Sensitive: true, 41 }, 42 "cluster_ca_certificate": &schema.Schema{ 43 Type: schema.TypeString, 44 Computed: true, 45 }, 46 "password": &schema.Schema{ 47 Type: schema.TypeString, 48 Required: true, 49 ForceNew: true, 50 Sensitive: true, 51 }, 52 "username": &schema.Schema{ 53 Type: schema.TypeString, 54 Required: true, 55 ForceNew: true, 56 }, 57 }, 58 }, 59 }, 60 61 "name": &schema.Schema{ 62 Type: schema.TypeString, 63 Required: true, 64 ForceNew: true, 65 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 66 value := v.(string) 67 68 if len(value) > 40 { 69 errors = append(errors, fmt.Errorf( 70 "%q cannot be longer than 40 characters", k)) 71 } 72 if !regexp.MustCompile("^[a-z0-9-]+$").MatchString(value) { 73 errors = append(errors, fmt.Errorf( 74 "%q can only contain lowercase letters, numbers and hyphens", k)) 75 } 76 if !regexp.MustCompile("^[a-z]").MatchString(value) { 77 errors = append(errors, fmt.Errorf( 78 "%q must start with a letter", k)) 79 } 80 if !regexp.MustCompile("[a-z0-9]$").MatchString(value) { 81 errors = append(errors, fmt.Errorf( 82 "%q must end with a number or a letter", k)) 83 } 84 return 85 }, 86 }, 87 88 "zone": &schema.Schema{ 89 Type: schema.TypeString, 90 Required: true, 91 ForceNew: true, 92 }, 93 94 "initial_node_count": &schema.Schema{ 95 Type: schema.TypeInt, 96 Optional: true, 97 ForceNew: true, 98 }, 99 100 "additional_zones": &schema.Schema{ 101 Type: schema.TypeList, 102 Optional: true, 103 Computed: true, 104 ForceNew: true, 105 Elem: &schema.Schema{Type: schema.TypeString}, 106 }, 107 108 "cluster_ipv4_cidr": &schema.Schema{ 109 Type: schema.TypeString, 110 Optional: true, 111 Computed: true, 112 ForceNew: true, 113 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 114 value := v.(string) 115 _, ipnet, err := net.ParseCIDR(value) 116 117 if err != nil || ipnet == nil || value != ipnet.String() { 118 errors = append(errors, fmt.Errorf( 119 "%q must contain a valid CIDR", k)) 120 } 121 return 122 }, 123 }, 124 125 "description": &schema.Schema{ 126 Type: schema.TypeString, 127 Optional: true, 128 ForceNew: true, 129 }, 130 131 "endpoint": &schema.Schema{ 132 Type: schema.TypeString, 133 Computed: true, 134 }, 135 136 "instance_group_urls": &schema.Schema{ 137 Type: schema.TypeList, 138 Computed: true, 139 Elem: &schema.Schema{Type: schema.TypeString}, 140 }, 141 142 "logging_service": &schema.Schema{ 143 Type: schema.TypeString, 144 Optional: true, 145 Computed: true, 146 ForceNew: true, 147 }, 148 149 "monitoring_service": &schema.Schema{ 150 Type: schema.TypeString, 151 Optional: true, 152 Computed: true, 153 ForceNew: true, 154 }, 155 156 "network": &schema.Schema{ 157 Type: schema.TypeString, 158 Optional: true, 159 Default: "default", 160 ForceNew: true, 161 }, 162 "subnetwork": &schema.Schema{ 163 Type: schema.TypeString, 164 Optional: true, 165 ForceNew: true, 166 }, 167 "addons_config": &schema.Schema{ 168 Type: schema.TypeList, 169 Optional: true, 170 ForceNew: true, 171 MaxItems: 1, 172 Elem: &schema.Resource{ 173 Schema: map[string]*schema.Schema{ 174 "http_load_balancing": &schema.Schema{ 175 Type: schema.TypeList, 176 Optional: true, 177 ForceNew: true, 178 MaxItems: 1, 179 Elem: &schema.Resource{ 180 Schema: map[string]*schema.Schema{ 181 "disabled": &schema.Schema{ 182 Type: schema.TypeBool, 183 Optional: true, 184 ForceNew: true, 185 }, 186 }, 187 }, 188 }, 189 "horizontal_pod_autoscaling": &schema.Schema{ 190 Type: schema.TypeList, 191 Optional: true, 192 ForceNew: true, 193 MaxItems: 1, 194 Elem: &schema.Resource{ 195 Schema: map[string]*schema.Schema{ 196 "disabled": &schema.Schema{ 197 Type: schema.TypeBool, 198 Optional: true, 199 ForceNew: true, 200 }, 201 }, 202 }, 203 }, 204 }, 205 }, 206 }, 207 "node_config": &schema.Schema{ 208 Type: schema.TypeList, 209 Optional: true, 210 Computed: true, 211 ForceNew: true, 212 Elem: &schema.Resource{ 213 Schema: map[string]*schema.Schema{ 214 "machine_type": &schema.Schema{ 215 Type: schema.TypeString, 216 Optional: true, 217 Computed: true, 218 ForceNew: true, 219 }, 220 221 "disk_size_gb": &schema.Schema{ 222 Type: schema.TypeInt, 223 Optional: true, 224 Computed: true, 225 ForceNew: true, 226 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 227 value := v.(int) 228 229 if value < 10 { 230 errors = append(errors, fmt.Errorf( 231 "%q cannot be less than 10", k)) 232 } 233 return 234 }, 235 }, 236 237 "local_ssd_count": &schema.Schema{ 238 Type: schema.TypeInt, 239 Optional: true, 240 Computed: true, 241 ForceNew: true, 242 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 243 value := v.(int) 244 245 if value < 0 { 246 errors = append(errors, fmt.Errorf( 247 "%q cannot be negative", k)) 248 } 249 return 250 }, 251 }, 252 253 "oauth_scopes": &schema.Schema{ 254 Type: schema.TypeList, 255 Optional: true, 256 Computed: true, 257 ForceNew: true, 258 Elem: &schema.Schema{ 259 Type: schema.TypeString, 260 StateFunc: func(v interface{}) string { 261 return canonicalizeServiceScope(v.(string)) 262 }, 263 }, 264 }, 265 266 "service_account": &schema.Schema{ 267 Type: schema.TypeString, 268 Optional: true, 269 Computed: true, 270 ForceNew: true, 271 }, 272 273 "metadata": &schema.Schema{ 274 Type: schema.TypeMap, 275 Optional: true, 276 ForceNew: true, 277 Elem: schema.TypeString, 278 }, 279 280 "image_type": &schema.Schema{ 281 Type: schema.TypeString, 282 Optional: true, 283 Computed: true, 284 ForceNew: true, 285 }, 286 }, 287 }, 288 }, 289 290 "node_version": &schema.Schema{ 291 Type: schema.TypeString, 292 Optional: true, 293 Computed: true, 294 }, 295 296 "node_pool": &schema.Schema{ 297 Type: schema.TypeList, 298 Optional: true, 299 Computed: true, 300 ForceNew: true, // TODO(danawillow): Add ability to add/remove nodePools 301 Elem: &schema.Resource{ 302 Schema: map[string]*schema.Schema{ 303 "initial_node_count": &schema.Schema{ 304 Type: schema.TypeInt, 305 Required: true, 306 ForceNew: true, 307 }, 308 309 "name": &schema.Schema{ 310 Type: schema.TypeString, 311 Optional: true, 312 Computed: true, 313 ConflictsWith: []string{"node_pool.name_prefix"}, 314 ForceNew: true, 315 }, 316 317 "name_prefix": &schema.Schema{ 318 Type: schema.TypeString, 319 Optional: true, 320 ForceNew: true, 321 }, 322 }, 323 }, 324 }, 325 326 "project": &schema.Schema{ 327 Type: schema.TypeString, 328 Optional: true, 329 ForceNew: true, 330 }, 331 }, 332 } 333 } 334 335 func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) error { 336 config := meta.(*Config) 337 338 project, err := getProject(d, config) 339 if err != nil { 340 return err 341 } 342 343 zoneName := d.Get("zone").(string) 344 clusterName := d.Get("name").(string) 345 346 masterAuths := d.Get("master_auth").([]interface{}) 347 if len(masterAuths) > 1 { 348 return fmt.Errorf("Cannot specify more than one master_auth.") 349 } 350 masterAuth := masterAuths[0].(map[string]interface{}) 351 352 cluster := &container.Cluster{ 353 MasterAuth: &container.MasterAuth{ 354 Password: masterAuth["password"].(string), 355 Username: masterAuth["username"].(string), 356 }, 357 Name: clusterName, 358 InitialNodeCount: int64(d.Get("initial_node_count").(int)), 359 } 360 361 if v, ok := d.GetOk("node_version"); ok { 362 cluster.InitialClusterVersion = v.(string) 363 } 364 365 if v, ok := d.GetOk("additional_zones"); ok { 366 locationsList := v.([]interface{}) 367 locations := []string{} 368 for _, v := range locationsList { 369 location := v.(string) 370 locations = append(locations, location) 371 if location == zoneName { 372 return fmt.Errorf("additional_zones should not contain the original 'zone'.") 373 } 374 } 375 locations = append(locations, zoneName) 376 cluster.Locations = locations 377 } 378 379 if v, ok := d.GetOk("cluster_ipv4_cidr"); ok { 380 cluster.ClusterIpv4Cidr = v.(string) 381 } 382 383 if v, ok := d.GetOk("description"); ok { 384 cluster.Description = v.(string) 385 } 386 387 if v, ok := d.GetOk("logging_service"); ok { 388 cluster.LoggingService = v.(string) 389 } 390 391 if v, ok := d.GetOk("monitoring_service"); ok { 392 cluster.MonitoringService = v.(string) 393 } 394 395 if _, ok := d.GetOk("network"); ok { 396 network, err := getNetworkName(d, "network") 397 if err != nil { 398 return err 399 } 400 cluster.Network = network 401 } 402 403 if v, ok := d.GetOk("subnetwork"); ok { 404 cluster.Subnetwork = v.(string) 405 } 406 407 if v, ok := d.GetOk("addons_config"); ok { 408 addonsConfig := v.([]interface{})[0].(map[string]interface{}) 409 cluster.AddonsConfig = &container.AddonsConfig{} 410 411 if v, ok := addonsConfig["http_load_balancing"]; ok && len(v.([]interface{})) > 0 { 412 addon := v.([]interface{})[0].(map[string]interface{}) 413 cluster.AddonsConfig.HttpLoadBalancing = &container.HttpLoadBalancing{ 414 Disabled: addon["disabled"].(bool), 415 } 416 } 417 418 if v, ok := addonsConfig["horizontal_pod_autoscaling"]; ok && len(v.([]interface{})) > 0 { 419 addon := v.([]interface{})[0].(map[string]interface{}) 420 cluster.AddonsConfig.HorizontalPodAutoscaling = &container.HorizontalPodAutoscaling{ 421 Disabled: addon["disabled"].(bool), 422 } 423 } 424 } 425 if v, ok := d.GetOk("node_config"); ok { 426 nodeConfigs := v.([]interface{}) 427 if len(nodeConfigs) > 1 { 428 return fmt.Errorf("Cannot specify more than one node_config.") 429 } 430 nodeConfig := nodeConfigs[0].(map[string]interface{}) 431 432 cluster.NodeConfig = &container.NodeConfig{} 433 434 if v, ok = nodeConfig["machine_type"]; ok { 435 cluster.NodeConfig.MachineType = v.(string) 436 } 437 438 if v, ok = nodeConfig["disk_size_gb"]; ok { 439 cluster.NodeConfig.DiskSizeGb = int64(v.(int)) 440 } 441 442 if v, ok = nodeConfig["local_ssd_count"]; ok { 443 cluster.NodeConfig.LocalSsdCount = int64(v.(int)) 444 } 445 446 if v, ok := nodeConfig["oauth_scopes"]; ok { 447 scopesList := v.([]interface{}) 448 scopes := []string{} 449 for _, v := range scopesList { 450 scopes = append(scopes, canonicalizeServiceScope(v.(string))) 451 } 452 453 cluster.NodeConfig.OauthScopes = scopes 454 } 455 456 if v, ok = nodeConfig["service_account"]; ok { 457 cluster.NodeConfig.ServiceAccount = v.(string) 458 } 459 460 if v, ok = nodeConfig["metadata"]; ok { 461 m := make(map[string]string) 462 for k, val := range v.(map[string]interface{}) { 463 m[k] = val.(string) 464 } 465 cluster.NodeConfig.Metadata = m 466 } 467 468 if v, ok = nodeConfig["image_type"]; ok { 469 cluster.NodeConfig.ImageType = v.(string) 470 } 471 } 472 473 nodePoolsCount := d.Get("node_pool.#").(int) 474 if nodePoolsCount > 0 { 475 nodePools := make([]*container.NodePool, 0, nodePoolsCount) 476 for i := 0; i < nodePoolsCount; i++ { 477 prefix := fmt.Sprintf("node_pool.%d", i) 478 479 nodeCount := d.Get(prefix + ".initial_node_count").(int) 480 481 var name string 482 if v, ok := d.GetOk(prefix + ".name"); ok { 483 name = v.(string) 484 } else if v, ok := d.GetOk(prefix + ".name_prefix"); ok { 485 name = resource.PrefixedUniqueId(v.(string)) 486 } else { 487 name = resource.UniqueId() 488 } 489 490 nodePool := &container.NodePool{ 491 Name: name, 492 InitialNodeCount: int64(nodeCount), 493 } 494 495 nodePools = append(nodePools, nodePool) 496 } 497 cluster.NodePools = nodePools 498 } 499 500 req := &container.CreateClusterRequest{ 501 Cluster: cluster, 502 } 503 504 op, err := config.clientContainer.Projects.Zones.Clusters.Create( 505 project, zoneName, req).Do() 506 if err != nil { 507 return err 508 } 509 510 // Wait until it's created 511 waitErr := containerOperationWait(config, op, project, zoneName, "creating GKE cluster", 30, 3) 512 if waitErr != nil { 513 // The resource didn't actually create 514 d.SetId("") 515 return waitErr 516 } 517 518 log.Printf("[INFO] GKE cluster %s has been created", clusterName) 519 520 d.SetId(clusterName) 521 522 return resourceContainerClusterRead(d, meta) 523 } 524 525 func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) error { 526 config := meta.(*Config) 527 528 project, err := getProject(d, config) 529 if err != nil { 530 return err 531 } 532 533 zoneName := d.Get("zone").(string) 534 535 cluster, err := config.clientContainer.Projects.Zones.Clusters.Get( 536 project, zoneName, d.Get("name").(string)).Do() 537 if err != nil { 538 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { 539 log.Printf("[WARN] Removing Container Cluster %q because it's gone", d.Get("name").(string)) 540 // The resource doesn't exist anymore 541 d.SetId("") 542 543 return nil 544 } 545 546 return err 547 } 548 549 d.Set("name", cluster.Name) 550 d.Set("zone", cluster.Zone) 551 552 locations := []string{} 553 if len(cluster.Locations) > 1 { 554 for _, location := range cluster.Locations { 555 if location != cluster.Zone { 556 locations = append(locations, location) 557 } 558 } 559 } 560 d.Set("additional_zones", locations) 561 562 d.Set("endpoint", cluster.Endpoint) 563 564 masterAuth := []map[string]interface{}{ 565 map[string]interface{}{ 566 "username": cluster.MasterAuth.Username, 567 "password": cluster.MasterAuth.Password, 568 "client_certificate": cluster.MasterAuth.ClientCertificate, 569 "client_key": cluster.MasterAuth.ClientKey, 570 "cluster_ca_certificate": cluster.MasterAuth.ClusterCaCertificate, 571 }, 572 } 573 d.Set("master_auth", masterAuth) 574 575 d.Set("initial_node_count", cluster.InitialNodeCount) 576 d.Set("node_version", cluster.CurrentNodeVersion) 577 d.Set("cluster_ipv4_cidr", cluster.ClusterIpv4Cidr) 578 d.Set("description", cluster.Description) 579 d.Set("logging_service", cluster.LoggingService) 580 d.Set("monitoring_service", cluster.MonitoringService) 581 d.Set("network", d.Get("network").(string)) 582 d.Set("subnetwork", cluster.Subnetwork) 583 d.Set("node_config", flattenClusterNodeConfig(cluster.NodeConfig)) 584 d.Set("node_pool", flattenClusterNodePools(d, cluster.NodePools)) 585 586 if igUrls, err := getInstanceGroupUrlsFromManagerUrls(config, cluster.InstanceGroupUrls); err != nil { 587 return err 588 } else { 589 d.Set("instance_group_urls", igUrls) 590 } 591 592 return nil 593 } 594 595 func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) error { 596 config := meta.(*Config) 597 598 project, err := getProject(d, config) 599 if err != nil { 600 return err 601 } 602 603 zoneName := d.Get("zone").(string) 604 clusterName := d.Get("name").(string) 605 desiredNodeVersion := d.Get("node_version").(string) 606 607 req := &container.UpdateClusterRequest{ 608 Update: &container.ClusterUpdate{ 609 DesiredNodeVersion: desiredNodeVersion, 610 }, 611 } 612 op, err := config.clientContainer.Projects.Zones.Clusters.Update( 613 project, zoneName, clusterName, req).Do() 614 if err != nil { 615 return err 616 } 617 618 // Wait until it's updated 619 waitErr := containerOperationWait(config, op, project, zoneName, "updating GKE cluster", 10, 2) 620 if waitErr != nil { 621 return waitErr 622 } 623 624 log.Printf("[INFO] GKE cluster %s has been updated to %s", d.Id(), 625 desiredNodeVersion) 626 627 return resourceContainerClusterRead(d, meta) 628 } 629 630 func resourceContainerClusterDelete(d *schema.ResourceData, meta interface{}) error { 631 config := meta.(*Config) 632 633 project, err := getProject(d, config) 634 if err != nil { 635 return err 636 } 637 638 zoneName := d.Get("zone").(string) 639 clusterName := d.Get("name").(string) 640 641 log.Printf("[DEBUG] Deleting GKE cluster %s", d.Get("name").(string)) 642 op, err := config.clientContainer.Projects.Zones.Clusters.Delete( 643 project, zoneName, clusterName).Do() 644 if err != nil { 645 return err 646 } 647 648 // Wait until it's deleted 649 waitErr := containerOperationWait(config, op, project, zoneName, "deleting GKE cluster", 10, 3) 650 if waitErr != nil { 651 return waitErr 652 } 653 654 log.Printf("[INFO] GKE cluster %s has been deleted", d.Id()) 655 656 d.SetId("") 657 658 return nil 659 } 660 661 // container engine's API currently mistakenly returns the instance group manager's 662 // URL instead of the instance group's URL in its responses. This shim detects that 663 // error, and corrects it, by fetching the instance group manager URL and retrieving 664 // the instance group manager, then using that to look up the instance group URL, which 665 // is then substituted. 666 // 667 // This should be removed when the API response is fixed. 668 func getInstanceGroupUrlsFromManagerUrls(config *Config, igmUrls []string) ([]string, error) { 669 instanceGroupURLs := make([]string, 0, len(igmUrls)) 670 for _, u := range igmUrls { 671 if !instanceGroupManagerURL.MatchString(u) { 672 instanceGroupURLs = append(instanceGroupURLs, u) 673 continue 674 } 675 matches := instanceGroupManagerURL.FindStringSubmatch(u) 676 instanceGroupManager, err := config.clientCompute.InstanceGroupManagers.Get(matches[1], matches[2], matches[3]).Do() 677 if err != nil { 678 return nil, fmt.Errorf("Error reading instance group manager returned as an instance group URL: %s", err) 679 } 680 instanceGroupURLs = append(instanceGroupURLs, instanceGroupManager.InstanceGroup) 681 } 682 return instanceGroupURLs, nil 683 } 684 685 func flattenClusterNodeConfig(c *container.NodeConfig) []map[string]interface{} { 686 config := []map[string]interface{}{ 687 map[string]interface{}{ 688 "machine_type": c.MachineType, 689 "disk_size_gb": c.DiskSizeGb, 690 "local_ssd_count": c.LocalSsdCount, 691 "service_account": c.ServiceAccount, 692 "metadata": c.Metadata, 693 "image_type": c.ImageType, 694 }, 695 } 696 697 if len(c.OauthScopes) > 0 { 698 config[0]["oauth_scopes"] = c.OauthScopes 699 } 700 701 return config 702 } 703 704 func flattenClusterNodePools(d *schema.ResourceData, c []*container.NodePool) []map[string]interface{} { 705 count := len(c) 706 707 nodePools := make([]map[string]interface{}, 0, count) 708 709 for i, np := range c { 710 nodePool := map[string]interface{}{ 711 "name": np.Name, 712 "name_prefix": d.Get(fmt.Sprintf("node_pool.%d.name_prefix", i)), 713 "initial_node_count": np.InitialNodeCount, 714 } 715 nodePools = append(nodePools, nodePool) 716 } 717 718 return nodePools 719 }