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  }