github.com/cbroglie/terraform@v0.7.0-rc3.0.20170410193827-735dfc416d46/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/schema" 10 "google.golang.org/api/container/v1" 11 "google.golang.org/api/googleapi" 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 "initial_node_count": &schema.Schema{ 27 Type: schema.TypeInt, 28 Required: true, 29 ForceNew: true, 30 }, 31 32 "master_auth": &schema.Schema{ 33 Type: schema.TypeList, 34 Required: true, 35 ForceNew: true, 36 Elem: &schema.Resource{ 37 Schema: map[string]*schema.Schema{ 38 "client_certificate": &schema.Schema{ 39 Type: schema.TypeString, 40 Computed: true, 41 }, 42 "client_key": &schema.Schema{ 43 Type: schema.TypeString, 44 Computed: true, 45 Sensitive: true, 46 }, 47 "cluster_ca_certificate": &schema.Schema{ 48 Type: schema.TypeString, 49 Computed: true, 50 }, 51 "password": &schema.Schema{ 52 Type: schema.TypeString, 53 Required: true, 54 ForceNew: true, 55 Sensitive: true, 56 }, 57 "username": &schema.Schema{ 58 Type: schema.TypeString, 59 Required: true, 60 ForceNew: true, 61 }, 62 }, 63 }, 64 }, 65 66 "name": &schema.Schema{ 67 Type: schema.TypeString, 68 Required: true, 69 ForceNew: true, 70 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 71 value := v.(string) 72 73 if len(value) > 40 { 74 errors = append(errors, fmt.Errorf( 75 "%q cannot be longer than 40 characters", k)) 76 } 77 if !regexp.MustCompile("^[a-z0-9-]+$").MatchString(value) { 78 errors = append(errors, fmt.Errorf( 79 "%q can only contain lowercase letters, numbers and hyphens", k)) 80 } 81 if !regexp.MustCompile("^[a-z]").MatchString(value) { 82 errors = append(errors, fmt.Errorf( 83 "%q must start with a letter", k)) 84 } 85 if !regexp.MustCompile("[a-z0-9]$").MatchString(value) { 86 errors = append(errors, fmt.Errorf( 87 "%q must end with a number or a letter", k)) 88 } 89 return 90 }, 91 }, 92 93 "zone": &schema.Schema{ 94 Type: schema.TypeString, 95 Required: 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 "project": &schema.Schema{ 296 Type: schema.TypeString, 297 Optional: true, 298 ForceNew: true, 299 }, 300 }, 301 } 302 } 303 304 func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) error { 305 config := meta.(*Config) 306 307 project, err := getProject(d, config) 308 if err != nil { 309 return err 310 } 311 312 zoneName := d.Get("zone").(string) 313 clusterName := d.Get("name").(string) 314 315 masterAuths := d.Get("master_auth").([]interface{}) 316 if len(masterAuths) > 1 { 317 return fmt.Errorf("Cannot specify more than one master_auth.") 318 } 319 masterAuth := masterAuths[0].(map[string]interface{}) 320 321 cluster := &container.Cluster{ 322 MasterAuth: &container.MasterAuth{ 323 Password: masterAuth["password"].(string), 324 Username: masterAuth["username"].(string), 325 }, 326 Name: clusterName, 327 InitialNodeCount: int64(d.Get("initial_node_count").(int)), 328 } 329 330 if v, ok := d.GetOk("node_version"); ok { 331 cluster.InitialClusterVersion = v.(string) 332 } 333 334 if v, ok := d.GetOk("additional_zones"); ok { 335 locationsList := v.([]interface{}) 336 locations := []string{} 337 for _, v := range locationsList { 338 location := v.(string) 339 locations = append(locations, location) 340 if location == zoneName { 341 return fmt.Errorf("additional_zones should not contain the original 'zone'.") 342 } 343 } 344 locations = append(locations, zoneName) 345 cluster.Locations = locations 346 } 347 348 if v, ok := d.GetOk("cluster_ipv4_cidr"); ok { 349 cluster.ClusterIpv4Cidr = v.(string) 350 } 351 352 if v, ok := d.GetOk("description"); ok { 353 cluster.Description = v.(string) 354 } 355 356 if v, ok := d.GetOk("logging_service"); ok { 357 cluster.LoggingService = v.(string) 358 } 359 360 if v, ok := d.GetOk("monitoring_service"); ok { 361 cluster.MonitoringService = v.(string) 362 } 363 364 if _, ok := d.GetOk("network"); ok { 365 network, err := getNetworkName(d, "network") 366 if err != nil { 367 return err 368 } 369 cluster.Network = network 370 } 371 372 if v, ok := d.GetOk("subnetwork"); ok { 373 cluster.Subnetwork = v.(string) 374 } 375 376 if v, ok := d.GetOk("addons_config"); ok { 377 addonsConfig := v.([]interface{})[0].(map[string]interface{}) 378 cluster.AddonsConfig = &container.AddonsConfig{} 379 380 if v, ok := addonsConfig["http_load_balancing"]; ok { 381 addon := v.([]interface{})[0].(map[string]interface{}) 382 cluster.AddonsConfig.HttpLoadBalancing = &container.HttpLoadBalancing{ 383 Disabled: addon["disabled"].(bool), 384 } 385 } 386 387 if v, ok := addonsConfig["horizontal_pod_autoscaling"]; ok { 388 addon := v.([]interface{})[0].(map[string]interface{}) 389 cluster.AddonsConfig.HorizontalPodAutoscaling = &container.HorizontalPodAutoscaling{ 390 Disabled: addon["disabled"].(bool), 391 } 392 } 393 } 394 if v, ok := d.GetOk("node_config"); ok { 395 nodeConfigs := v.([]interface{}) 396 if len(nodeConfigs) > 1 { 397 return fmt.Errorf("Cannot specify more than one node_config.") 398 } 399 nodeConfig := nodeConfigs[0].(map[string]interface{}) 400 401 cluster.NodeConfig = &container.NodeConfig{} 402 403 if v, ok = nodeConfig["machine_type"]; ok { 404 cluster.NodeConfig.MachineType = v.(string) 405 } 406 407 if v, ok = nodeConfig["disk_size_gb"]; ok { 408 cluster.NodeConfig.DiskSizeGb = int64(v.(int)) 409 } 410 411 if v, ok = nodeConfig["local_ssd_count"]; ok { 412 cluster.NodeConfig.LocalSsdCount = int64(v.(int)) 413 } 414 415 if v, ok := nodeConfig["oauth_scopes"]; ok { 416 scopesList := v.([]interface{}) 417 scopes := []string{} 418 for _, v := range scopesList { 419 scopes = append(scopes, canonicalizeServiceScope(v.(string))) 420 } 421 422 cluster.NodeConfig.OauthScopes = scopes 423 } 424 425 if v, ok = nodeConfig["service_account"]; ok { 426 cluster.NodeConfig.ServiceAccount = v.(string) 427 } 428 429 if v, ok = nodeConfig["metadata"]; ok { 430 m := make(map[string]string) 431 for k, val := range v.(map[string]interface{}) { 432 m[k] = val.(string) 433 } 434 cluster.NodeConfig.Metadata = m 435 } 436 437 if v, ok = nodeConfig["image_type"]; ok { 438 cluster.NodeConfig.ImageType = v.(string) 439 } 440 } 441 442 req := &container.CreateClusterRequest{ 443 Cluster: cluster, 444 } 445 446 op, err := config.clientContainer.Projects.Zones.Clusters.Create( 447 project, zoneName, req).Do() 448 if err != nil { 449 return err 450 } 451 452 // Wait until it's created 453 waitErr := containerOperationWait(config, op, project, zoneName, "creating GKE cluster", 30, 3) 454 if waitErr != nil { 455 // The resource didn't actually create 456 d.SetId("") 457 return waitErr 458 } 459 460 log.Printf("[INFO] GKE cluster %s has been created", clusterName) 461 462 d.SetId(clusterName) 463 464 return resourceContainerClusterRead(d, meta) 465 } 466 467 func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) error { 468 config := meta.(*Config) 469 470 project, err := getProject(d, config) 471 if err != nil { 472 return err 473 } 474 475 zoneName := d.Get("zone").(string) 476 477 cluster, err := config.clientContainer.Projects.Zones.Clusters.Get( 478 project, zoneName, d.Get("name").(string)).Do() 479 if err != nil { 480 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { 481 log.Printf("[WARN] Removing Container Cluster %q because it's gone", d.Get("name").(string)) 482 // The resource doesn't exist anymore 483 d.SetId("") 484 485 return nil 486 } 487 488 return err 489 } 490 491 d.Set("name", cluster.Name) 492 d.Set("zone", cluster.Zone) 493 494 locations := []string{} 495 if len(cluster.Locations) > 1 { 496 for _, location := range cluster.Locations { 497 if location != cluster.Zone { 498 locations = append(locations, location) 499 } 500 } 501 } 502 d.Set("additional_zones", locations) 503 504 d.Set("endpoint", cluster.Endpoint) 505 506 masterAuth := []map[string]interface{}{ 507 map[string]interface{}{ 508 "username": cluster.MasterAuth.Username, 509 "password": cluster.MasterAuth.Password, 510 "client_certificate": cluster.MasterAuth.ClientCertificate, 511 "client_key": cluster.MasterAuth.ClientKey, 512 "cluster_ca_certificate": cluster.MasterAuth.ClusterCaCertificate, 513 }, 514 } 515 d.Set("master_auth", masterAuth) 516 517 d.Set("initial_node_count", cluster.InitialNodeCount) 518 d.Set("node_version", cluster.CurrentNodeVersion) 519 d.Set("cluster_ipv4_cidr", cluster.ClusterIpv4Cidr) 520 d.Set("description", cluster.Description) 521 d.Set("logging_service", cluster.LoggingService) 522 d.Set("monitoring_service", cluster.MonitoringService) 523 d.Set("network", d.Get("network").(string)) 524 d.Set("subnetwork", cluster.Subnetwork) 525 d.Set("node_config", flattenClusterNodeConfig(cluster.NodeConfig)) 526 527 if igUrls, err := getInstanceGroupUrlsFromManagerUrls(config, cluster.InstanceGroupUrls); err != nil { 528 return err 529 } else { 530 d.Set("instance_group_urls", igUrls) 531 } 532 533 return nil 534 } 535 536 func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) error { 537 config := meta.(*Config) 538 539 project, err := getProject(d, config) 540 if err != nil { 541 return err 542 } 543 544 zoneName := d.Get("zone").(string) 545 clusterName := d.Get("name").(string) 546 desiredNodeVersion := d.Get("node_version").(string) 547 548 req := &container.UpdateClusterRequest{ 549 Update: &container.ClusterUpdate{ 550 DesiredNodeVersion: desiredNodeVersion, 551 }, 552 } 553 op, err := config.clientContainer.Projects.Zones.Clusters.Update( 554 project, zoneName, clusterName, req).Do() 555 if err != nil { 556 return err 557 } 558 559 // Wait until it's updated 560 waitErr := containerOperationWait(config, op, project, zoneName, "updating GKE cluster", 10, 2) 561 if waitErr != nil { 562 return waitErr 563 } 564 565 log.Printf("[INFO] GKE cluster %s has been updated to %s", d.Id(), 566 desiredNodeVersion) 567 568 return resourceContainerClusterRead(d, meta) 569 } 570 571 func resourceContainerClusterDelete(d *schema.ResourceData, meta interface{}) error { 572 config := meta.(*Config) 573 574 project, err := getProject(d, config) 575 if err != nil { 576 return err 577 } 578 579 zoneName := d.Get("zone").(string) 580 clusterName := d.Get("name").(string) 581 582 log.Printf("[DEBUG] Deleting GKE cluster %s", d.Get("name").(string)) 583 op, err := config.clientContainer.Projects.Zones.Clusters.Delete( 584 project, zoneName, clusterName).Do() 585 if err != nil { 586 return err 587 } 588 589 // Wait until it's deleted 590 waitErr := containerOperationWait(config, op, project, zoneName, "deleting GKE cluster", 10, 3) 591 if waitErr != nil { 592 return waitErr 593 } 594 595 log.Printf("[INFO] GKE cluster %s has been deleted", d.Id()) 596 597 d.SetId("") 598 599 return nil 600 } 601 602 // container engine's API currently mistakenly returns the instance group manager's 603 // URL instead of the instance group's URL in its responses. This shim detects that 604 // error, and corrects it, by fetching the instance group manager URL and retrieving 605 // the instance group manager, then using that to look up the instance group URL, which 606 // is then substituted. 607 // 608 // This should be removed when the API response is fixed. 609 func getInstanceGroupUrlsFromManagerUrls(config *Config, igmUrls []string) ([]string, error) { 610 instanceGroupURLs := make([]string, 0, len(igmUrls)) 611 for _, u := range igmUrls { 612 if !instanceGroupManagerURL.MatchString(u) { 613 instanceGroupURLs = append(instanceGroupURLs, u) 614 continue 615 } 616 matches := instanceGroupManagerURL.FindStringSubmatch(u) 617 instanceGroupManager, err := config.clientCompute.InstanceGroupManagers.Get(matches[1], matches[2], matches[3]).Do() 618 if err != nil { 619 return nil, fmt.Errorf("Error reading instance group manager returned as an instance group URL: %s", err) 620 } 621 instanceGroupURLs = append(instanceGroupURLs, instanceGroupManager.InstanceGroup) 622 } 623 return instanceGroupURLs, nil 624 } 625 626 func flattenClusterNodeConfig(c *container.NodeConfig) []map[string]interface{} { 627 config := []map[string]interface{}{ 628 map[string]interface{}{ 629 "machine_type": c.MachineType, 630 "disk_size_gb": c.DiskSizeGb, 631 "local_ssd_count": c.LocalSsdCount, 632 "service_account": c.ServiceAccount, 633 "metadata": c.Metadata, 634 "image_type": c.ImageType, 635 }, 636 } 637 638 if len(c.OauthScopes) > 0 { 639 config[0]["oauth_scopes"] = c.OauthScopes 640 } 641 642 return config 643 }