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  }