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