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