github.com/peterbale/terraform@v0.9.0-beta2.0.20170315142748-5723acd55547/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 }, 46 "cluster_ca_certificate": &schema.Schema{ 47 Type: schema.TypeString, 48 Computed: true, 49 }, 50 "password": &schema.Schema{ 51 Type: schema.TypeString, 52 Required: true, 53 ForceNew: true, 54 }, 55 "username": &schema.Schema{ 56 Type: schema.TypeString, 57 Required: true, 58 ForceNew: true, 59 }, 60 }, 61 }, 62 }, 63 64 "name": &schema.Schema{ 65 Type: schema.TypeString, 66 Required: true, 67 ForceNew: true, 68 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 69 value := v.(string) 70 71 if len(value) > 40 { 72 errors = append(errors, fmt.Errorf( 73 "%q cannot be longer than 40 characters", k)) 74 } 75 if !regexp.MustCompile("^[a-z0-9-]+$").MatchString(value) { 76 errors = append(errors, fmt.Errorf( 77 "%q can only contain lowercase letters, numbers and hyphens", k)) 78 } 79 if !regexp.MustCompile("^[a-z]").MatchString(value) { 80 errors = append(errors, fmt.Errorf( 81 "%q must start with a letter", k)) 82 } 83 if !regexp.MustCompile("[a-z0-9]$").MatchString(value) { 84 errors = append(errors, fmt.Errorf( 85 "%q must end with a number or a letter", k)) 86 } 87 return 88 }, 89 }, 90 91 "zone": &schema.Schema{ 92 Type: schema.TypeString, 93 Required: true, 94 ForceNew: true, 95 }, 96 97 "additional_zones": &schema.Schema{ 98 Type: schema.TypeList, 99 Optional: true, 100 Computed: true, 101 ForceNew: true, 102 Elem: &schema.Schema{Type: schema.TypeString}, 103 }, 104 105 "cluster_ipv4_cidr": &schema.Schema{ 106 Type: schema.TypeString, 107 Optional: true, 108 Computed: true, 109 ForceNew: true, 110 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 111 value := v.(string) 112 _, ipnet, err := net.ParseCIDR(value) 113 114 if err != nil || ipnet == nil || value != ipnet.String() { 115 errors = append(errors, fmt.Errorf( 116 "%q must contain a valid CIDR", k)) 117 } 118 return 119 }, 120 }, 121 122 "description": &schema.Schema{ 123 Type: schema.TypeString, 124 Optional: true, 125 ForceNew: true, 126 }, 127 128 "endpoint": &schema.Schema{ 129 Type: schema.TypeString, 130 Computed: true, 131 }, 132 133 "instance_group_urls": &schema.Schema{ 134 Type: schema.TypeList, 135 Computed: true, 136 Elem: &schema.Schema{Type: schema.TypeString}, 137 }, 138 139 "logging_service": &schema.Schema{ 140 Type: schema.TypeString, 141 Optional: true, 142 Computed: true, 143 ForceNew: true, 144 }, 145 146 "monitoring_service": &schema.Schema{ 147 Type: schema.TypeString, 148 Optional: true, 149 Computed: true, 150 ForceNew: true, 151 }, 152 153 "network": &schema.Schema{ 154 Type: schema.TypeString, 155 Optional: true, 156 Default: "default", 157 ForceNew: true, 158 }, 159 "subnetwork": &schema.Schema{ 160 Type: schema.TypeString, 161 Optional: true, 162 ForceNew: true, 163 }, 164 "addons_config": &schema.Schema{ 165 Type: schema.TypeList, 166 Optional: true, 167 ForceNew: true, 168 MaxItems: 1, 169 Elem: &schema.Resource{ 170 Schema: map[string]*schema.Schema{ 171 "http_load_balancing": &schema.Schema{ 172 Type: schema.TypeList, 173 Optional: true, 174 ForceNew: true, 175 MaxItems: 1, 176 Elem: &schema.Resource{ 177 Schema: map[string]*schema.Schema{ 178 "disabled": &schema.Schema{ 179 Type: schema.TypeBool, 180 Optional: true, 181 ForceNew: true, 182 }, 183 }, 184 }, 185 }, 186 "horizontal_pod_autoscaling": &schema.Schema{ 187 Type: schema.TypeList, 188 Optional: true, 189 ForceNew: true, 190 MaxItems: 1, 191 Elem: &schema.Resource{ 192 Schema: map[string]*schema.Schema{ 193 "disabled": &schema.Schema{ 194 Type: schema.TypeBool, 195 Optional: true, 196 ForceNew: true, 197 }, 198 }, 199 }, 200 }, 201 }, 202 }, 203 }, 204 "node_config": &schema.Schema{ 205 Type: schema.TypeList, 206 Optional: true, 207 Computed: true, 208 ForceNew: true, 209 Elem: &schema.Resource{ 210 Schema: map[string]*schema.Schema{ 211 "machine_type": &schema.Schema{ 212 Type: schema.TypeString, 213 Optional: true, 214 Computed: true, 215 ForceNew: true, 216 }, 217 218 "disk_size_gb": &schema.Schema{ 219 Type: schema.TypeInt, 220 Optional: true, 221 Computed: true, 222 ForceNew: true, 223 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 224 value := v.(int) 225 226 if value < 10 { 227 errors = append(errors, fmt.Errorf( 228 "%q cannot be less than 10", k)) 229 } 230 return 231 }, 232 }, 233 234 "oauth_scopes": &schema.Schema{ 235 Type: schema.TypeList, 236 Optional: true, 237 Computed: true, 238 ForceNew: true, 239 Elem: &schema.Schema{ 240 Type: schema.TypeString, 241 StateFunc: func(v interface{}) string { 242 return canonicalizeServiceScope(v.(string)) 243 }, 244 }, 245 }, 246 }, 247 }, 248 }, 249 250 "node_version": &schema.Schema{ 251 Type: schema.TypeString, 252 Optional: true, 253 Computed: true, 254 }, 255 256 "project": &schema.Schema{ 257 Type: schema.TypeString, 258 Optional: true, 259 ForceNew: true, 260 }, 261 }, 262 } 263 } 264 265 func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) error { 266 config := meta.(*Config) 267 268 project, err := getProject(d, config) 269 if err != nil { 270 return err 271 } 272 273 zoneName := d.Get("zone").(string) 274 clusterName := d.Get("name").(string) 275 276 masterAuths := d.Get("master_auth").([]interface{}) 277 if len(masterAuths) > 1 { 278 return fmt.Errorf("Cannot specify more than one master_auth.") 279 } 280 masterAuth := masterAuths[0].(map[string]interface{}) 281 282 cluster := &container.Cluster{ 283 MasterAuth: &container.MasterAuth{ 284 Password: masterAuth["password"].(string), 285 Username: masterAuth["username"].(string), 286 }, 287 Name: clusterName, 288 InitialNodeCount: int64(d.Get("initial_node_count").(int)), 289 } 290 291 if v, ok := d.GetOk("node_version"); ok { 292 cluster.InitialClusterVersion = v.(string) 293 } 294 295 if v, ok := d.GetOk("additional_zones"); ok { 296 locationsList := v.([]interface{}) 297 locations := []string{} 298 for _, v := range locationsList { 299 location := v.(string) 300 locations = append(locations, location) 301 if location == zoneName { 302 return fmt.Errorf("additional_zones should not contain the original 'zone'.") 303 } 304 } 305 locations = append(locations, zoneName) 306 cluster.Locations = locations 307 } 308 309 if v, ok := d.GetOk("cluster_ipv4_cidr"); ok { 310 cluster.ClusterIpv4Cidr = v.(string) 311 } 312 313 if v, ok := d.GetOk("description"); ok { 314 cluster.Description = v.(string) 315 } 316 317 if v, ok := d.GetOk("logging_service"); ok { 318 cluster.LoggingService = v.(string) 319 } 320 321 if v, ok := d.GetOk("monitoring_service"); ok { 322 cluster.MonitoringService = v.(string) 323 } 324 325 if _, ok := d.GetOk("network"); ok { 326 network, err := getNetworkName(d, "network") 327 if err != nil { 328 return err 329 } 330 cluster.Network = network 331 } 332 333 if v, ok := d.GetOk("subnetwork"); ok { 334 cluster.Subnetwork = v.(string) 335 } 336 337 if v, ok := d.GetOk("addons_config"); ok { 338 addonsConfig := v.([]interface{})[0].(map[string]interface{}) 339 cluster.AddonsConfig = &container.AddonsConfig{} 340 341 if v, ok := addonsConfig["http_load_balancing"]; ok { 342 addon := v.([]interface{})[0].(map[string]interface{}) 343 cluster.AddonsConfig.HttpLoadBalancing = &container.HttpLoadBalancing{ 344 Disabled: addon["disabled"].(bool), 345 } 346 } 347 348 if v, ok := addonsConfig["horizontal_pod_autoscaling"]; ok { 349 addon := v.([]interface{})[0].(map[string]interface{}) 350 cluster.AddonsConfig.HorizontalPodAutoscaling = &container.HorizontalPodAutoscaling{ 351 Disabled: addon["disabled"].(bool), 352 } 353 } 354 } 355 if v, ok := d.GetOk("node_config"); ok { 356 nodeConfigs := v.([]interface{}) 357 if len(nodeConfigs) > 1 { 358 return fmt.Errorf("Cannot specify more than one node_config.") 359 } 360 nodeConfig := nodeConfigs[0].(map[string]interface{}) 361 362 cluster.NodeConfig = &container.NodeConfig{} 363 364 if v, ok = nodeConfig["machine_type"]; ok { 365 cluster.NodeConfig.MachineType = v.(string) 366 } 367 368 if v, ok = nodeConfig["disk_size_gb"]; ok { 369 cluster.NodeConfig.DiskSizeGb = int64(v.(int)) 370 } 371 372 if v, ok := nodeConfig["oauth_scopes"]; ok { 373 scopesList := v.([]interface{}) 374 scopes := []string{} 375 for _, v := range scopesList { 376 scopes = append(scopes, canonicalizeServiceScope(v.(string))) 377 } 378 379 cluster.NodeConfig.OauthScopes = scopes 380 } 381 } 382 383 req := &container.CreateClusterRequest{ 384 Cluster: cluster, 385 } 386 387 op, err := config.clientContainer.Projects.Zones.Clusters.Create( 388 project, zoneName, req).Do() 389 if err != nil { 390 return err 391 } 392 393 // Wait until it's created 394 waitErr := containerOperationWait(config, op, project, zoneName, "creating GKE cluster", 30, 3) 395 if waitErr != nil { 396 // The resource didn't actually create 397 d.SetId("") 398 return waitErr 399 } 400 401 log.Printf("[INFO] GKE cluster %s has been created", clusterName) 402 403 d.SetId(clusterName) 404 405 return resourceContainerClusterRead(d, meta) 406 } 407 408 func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) error { 409 config := meta.(*Config) 410 411 project, err := getProject(d, config) 412 if err != nil { 413 return err 414 } 415 416 zoneName := d.Get("zone").(string) 417 418 cluster, err := config.clientContainer.Projects.Zones.Clusters.Get( 419 project, zoneName, d.Get("name").(string)).Do() 420 if err != nil { 421 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { 422 log.Printf("[WARN] Removing Container Cluster %q because it's gone", d.Get("name").(string)) 423 // The resource doesn't exist anymore 424 d.SetId("") 425 426 return nil 427 } 428 429 return err 430 } 431 432 d.Set("name", cluster.Name) 433 d.Set("zone", cluster.Zone) 434 435 locations := []string{} 436 if len(cluster.Locations) > 1 { 437 for _, location := range cluster.Locations { 438 if location != cluster.Zone { 439 locations = append(locations, location) 440 } 441 } 442 } 443 d.Set("additional_zones", locations) 444 445 d.Set("endpoint", cluster.Endpoint) 446 447 masterAuth := []map[string]interface{}{ 448 map[string]interface{}{ 449 "username": cluster.MasterAuth.Username, 450 "password": cluster.MasterAuth.Password, 451 "client_certificate": cluster.MasterAuth.ClientCertificate, 452 "client_key": cluster.MasterAuth.ClientKey, 453 "cluster_ca_certificate": cluster.MasterAuth.ClusterCaCertificate, 454 }, 455 } 456 d.Set("master_auth", masterAuth) 457 458 d.Set("initial_node_count", cluster.InitialNodeCount) 459 d.Set("node_version", cluster.CurrentNodeVersion) 460 d.Set("cluster_ipv4_cidr", cluster.ClusterIpv4Cidr) 461 d.Set("description", cluster.Description) 462 d.Set("logging_service", cluster.LoggingService) 463 d.Set("monitoring_service", cluster.MonitoringService) 464 d.Set("network", d.Get("network").(string)) 465 d.Set("subnetwork", cluster.Subnetwork) 466 d.Set("node_config", flattenClusterNodeConfig(cluster.NodeConfig)) 467 468 // container engine's API currently mistakenly returns the instance group manager's 469 // URL instead of the instance group's URL in its responses. This shim detects that 470 // error, and corrects it, by fetching the instance group manager URL and retrieving 471 // the instance group manager, then using that to look up the instance group URL, which 472 // is then substituted. 473 // 474 // This should be removed when the API response is fixed. 475 instanceGroupURLs := make([]string, 0, len(cluster.InstanceGroupUrls)) 476 for _, u := range cluster.InstanceGroupUrls { 477 if !instanceGroupManagerURL.MatchString(u) { 478 instanceGroupURLs = append(instanceGroupURLs, u) 479 continue 480 } 481 matches := instanceGroupManagerURL.FindStringSubmatch(u) 482 instanceGroupManager, err := config.clientCompute.InstanceGroupManagers.Get(matches[1], matches[2], matches[3]).Do() 483 if err != nil { 484 return fmt.Errorf("Error reading instance group manager returned as an instance group URL: %s", err) 485 } 486 instanceGroupURLs = append(instanceGroupURLs, instanceGroupManager.InstanceGroup) 487 } 488 d.Set("instance_group_urls", instanceGroupURLs) 489 490 return nil 491 } 492 493 func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) error { 494 config := meta.(*Config) 495 496 project, err := getProject(d, config) 497 if err != nil { 498 return err 499 } 500 501 zoneName := d.Get("zone").(string) 502 clusterName := d.Get("name").(string) 503 desiredNodeVersion := d.Get("node_version").(string) 504 505 req := &container.UpdateClusterRequest{ 506 Update: &container.ClusterUpdate{ 507 DesiredNodeVersion: desiredNodeVersion, 508 }, 509 } 510 op, err := config.clientContainer.Projects.Zones.Clusters.Update( 511 project, zoneName, clusterName, req).Do() 512 if err != nil { 513 return err 514 } 515 516 // Wait until it's updated 517 waitErr := containerOperationWait(config, op, project, zoneName, "updating GKE cluster", 10, 2) 518 if waitErr != nil { 519 return waitErr 520 } 521 522 log.Printf("[INFO] GKE cluster %s has been updated to %s", d.Id(), 523 desiredNodeVersion) 524 525 return resourceContainerClusterRead(d, meta) 526 } 527 528 func resourceContainerClusterDelete(d *schema.ResourceData, meta interface{}) error { 529 config := meta.(*Config) 530 531 project, err := getProject(d, config) 532 if err != nil { 533 return err 534 } 535 536 zoneName := d.Get("zone").(string) 537 clusterName := d.Get("name").(string) 538 539 log.Printf("[DEBUG] Deleting GKE cluster %s", d.Get("name").(string)) 540 op, err := config.clientContainer.Projects.Zones.Clusters.Delete( 541 project, zoneName, clusterName).Do() 542 if err != nil { 543 return err 544 } 545 546 // Wait until it's deleted 547 waitErr := containerOperationWait(config, op, project, zoneName, "deleting GKE cluster", 10, 3) 548 if waitErr != nil { 549 return waitErr 550 } 551 552 log.Printf("[INFO] GKE cluster %s has been deleted", d.Id()) 553 554 d.SetId("") 555 556 return nil 557 } 558 559 func flattenClusterNodeConfig(c *container.NodeConfig) []map[string]interface{} { 560 config := []map[string]interface{}{ 561 map[string]interface{}{ 562 "machine_type": c.MachineType, 563 "disk_size_gb": c.DiskSizeGb, 564 }, 565 } 566 567 if len(c.OauthScopes) > 0 { 568 config[0]["oauth_scopes"] = c.OauthScopes 569 } 570 571 return config 572 }