diff --git a/google/resource_container_cluster.go b/google/resource_container_cluster.go index 88e9f24341c..b53469b716e 100644 --- a/google/resource_container_cluster.go +++ b/google/resource_container_cluster.go @@ -3,7 +3,6 @@ package google import ( "fmt" "log" - "net" "regexp" "strings" "time" @@ -215,6 +214,30 @@ func resourceContainerCluster() *schema.Resource { }, }, + "master_authorized_networks_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + "cidr_blocks": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 10, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.CIDRNetwork(0, 32), + }, + }, + }, + }, + }, + "min_master_version": { Type: schema.TypeString, Optional: true, @@ -310,6 +333,10 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er } } + if v, ok := d.GetOk("master_authorized_networks_config"); ok { + cluster.MasterAuthorizedNetworksConfig = expandMasterAuthorizedNetworksConfig(v) + } + if v, ok := d.GetOk("min_master_version"); ok { cluster.InitialClusterVersion = v.(string) } @@ -471,6 +498,10 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro } d.Set("master_auth", masterAuth) + if cluster.MasterAuthorizedNetworksConfig != nil { + d.Set("master_authorized_networks_config", flattenMasterAuthorizedNetworksConfig(cluster.MasterAuthorizedNetworksConfig)) + } + d.Set("initial_node_count", cluster.InitialNodeCount) d.Set("master_version", cluster.CurrentMasterVersion) d.Set("node_version", cluster.CurrentNodeVersion) @@ -514,6 +545,30 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er d.Partial(true) + if d.HasChange("master_authorized_networks_config") { + if c, ok := d.GetOk("master_authorized_networks_config"); ok { + req := &container.UpdateClusterRequest{ + Update: &container.ClusterUpdate{ + DesiredMasterAuthorizedNetworksConfig: expandMasterAuthorizedNetworksConfig(c), + }, + } + op, err := config.clientContainer.Projects.Zones.Clusters.Update( + project, zoneName, clusterName, req).Do() + if err != nil { + return err + } + + // Wait until it's updated + waitErr := containerOperationWait(config, op, project, zoneName, "updating GKE cluster master authorized networks", timeoutInMinutes, 2) + if waitErr != nil { + return waitErr + } + log.Printf("[INFO] GKE cluster %s master authorized networks config has been updated", d.Id()) + + d.SetPartial("master_authorized_networks_config") + } + } + // The master must be updated before the nodes if d.HasChange("min_master_version") { desiredMasterVersion := d.Get("min_master_version").(string) @@ -804,6 +859,23 @@ func expandClusterAddonsConfig(configured interface{}) *container.AddonsConfig { return ac } +func expandMasterAuthorizedNetworksConfig(configured interface{}) *container.MasterAuthorizedNetworksConfig { + config := configured.([]interface{})[0].(map[string]interface{}) + cidrBlocks := config["cidr_blocks"].(*schema.Set).List() + result := &container.MasterAuthorizedNetworksConfig{ + Enabled: config["enabled"].(bool), + CidrBlocks: make([]*container.CidrBlock, 0), + } + if result.Enabled { + for _, v := range cidrBlocks { + result.CidrBlocks = append(result.CidrBlocks, &container.CidrBlock{ + CidrBlock: v.(string), + }) + } + } + return result +} + func flattenClusterAddonsConfig(c *container.AddonsConfig) []map[string]interface{} { result := make(map[string]interface{}) if c.HorizontalPodAutoscaling != nil { @@ -844,6 +916,20 @@ func flattenClusterNodePools(d *schema.ResourceData, config *Config, c []*contai return nodePools, nil } +func flattenMasterAuthorizedNetworksConfig(c *container.MasterAuthorizedNetworksConfig) []map[string]interface{} { + cidrBlocks := make([]string, 0, len(c.CidrBlocks)) + for _, v := range c.CidrBlocks { + cidrBlocks = append(cidrBlocks, v.CidrBlock) + } + result := []map[string]interface{}{ + map[string]interface{}{ + "enabled": c.Enabled, + "cidr_blocks": cidrBlocks, + }, + } + return result +} + func resourceContainerClusterStateImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { parts := strings.Split(d.Id(), "/") if len(parts) != 2 { diff --git a/google/resource_container_cluster_test.go b/google/resource_container_cluster_test.go index fc829d6c7f2..50dca103b6c 100644 --- a/google/resource_container_cluster_test.go +++ b/google/resource_container_cluster_test.go @@ -106,6 +106,50 @@ func TestAccContainerCluster_withMasterAuth(t *testing.T) { }) } +func TestAccContainerCluster_withMasterAuthorizedNetworksConfig(t *testing.T) { + t.Parallel() + + clusterName := fmt.Sprintf("cluster-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckContainerClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName, true, []string{"0.0.0.0/0"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckContainerCluster("google_container_cluster.with_master_authorized_networks"), + resource.TestCheckResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.enabled", "true"), + resource.TestCheckResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.cidr_blocks.#", "1"), + ), + }, + { + Config: testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName, true, []string{}), + Check: resource.ComposeTestCheckFunc( + testAccCheckContainerCluster("google_container_cluster.with_master_authorized_networks"), + resource.TestCheckResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.enabled", "true"), + resource.TestCheckNoResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.cidr_blocks"), + ), + }, + { + Config: testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName, false, []string{"8.8.8.8/32"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckContainerCluster("google_container_cluster.with_master_authorized_networks"), + resource.TestCheckResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.enabled", "false"), + resource.TestCheckNoResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.cidr_blocks"), + ), + }, + }, + }) +} + func TestAccContainerCluster_withAdditionalZones(t *testing.T) { t.Parallel() @@ -656,6 +700,13 @@ func testAccCheckContainerCluster(n string) resource.TestCheckFunc { } } + masterAuthorizedNetworksEnabled := false + if cluster.MasterAuthorizedNetworksConfig != nil { + masterAuthorizedNetworksEnabled = cluster.MasterAuthorizedNetworksConfig.Enabled + clusterTests = append(clusterTests, + clusterTestField{"master_authorized_networks_config.0.enabled", masterAuthorizedNetworksEnabled}) + } + for _, attrs := range clusterTests { if c := checkMatch(attributes, attrs.tf_attr, attrs.gcp_attr); c != "" { return fmt.Errorf(c) @@ -859,6 +910,25 @@ resource "google_container_cluster" "with_master_auth" { } }`, acctest.RandString(10)) +func testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName string, enabled bool, cidrBlocks []string) string { + + for i, cidr := range cidrBlocks { + cidrBlocks[i] = strconv.Quote(cidr) + } + + return fmt.Sprintf(` +resource "google_container_cluster" "with_master_authorized_networks" { + name = "%s" + zone = "us-central1-a" + initial_node_count = 1 + + master_authorized_networks_config { + enabled = %v + cidr_blocks = [%s] + } +}`, clusterName, enabled, strings.Join(cidrBlocks, ",")) +} + func testAccContainerCluster_withAdditionalZones(clusterName string) string { return fmt.Sprintf(` resource "google_container_cluster" "with_additional_zones" { diff --git a/google/validation.go b/google/validation.go index 1feebf1a322..cf1f1ddd404 100644 --- a/google/validation.go +++ b/google/validation.go @@ -3,6 +3,8 @@ package google import ( "fmt" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "net" "regexp" ) @@ -15,6 +17,12 @@ const ( SubnetworkLinkRegex = "projects/(" + ProjectRegex + ")/regions/(" + RegionRegex + ")/subnetworks/(" + SubnetworkRegex + ")$" ) +var rfc1918Networks = []string{ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", +} + func validateGCPName(v interface{}, k string) (ws []string, errors []error) { re := `^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$` return validateRegexp(re)(v, k) @@ -31,3 +39,25 @@ func validateRegexp(re string) schema.SchemaValidateFunc { return } } + +func validateRFC1918Network(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + + s, es = validation.CIDRNetwork(min, max)(i, k) + if len(es) > 0 { + return + } + + v, _ := i.(string) + ip, _, _ := net.ParseCIDR(v) + for _, c := range rfc1918Networks { + if _, ipnet, _ := net.ParseCIDR(c); ipnet.Contains(ip) { + return + } + } + + es = append(es, fmt.Errorf("expected %q to be an RFC1918-compliant CIDR, got: %s", k, v)) + + return + } +} diff --git a/website/docs/r/container_cluster.html.markdown b/website/docs/r/container_cluster.html.markdown index 5ef6d91f73c..169deced1be 100644 --- a/website/docs/r/container_cluster.html.markdown +++ b/website/docs/r/container_cluster.html.markdown @@ -41,11 +41,11 @@ resource "google_container_cluster" "primary" { "https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/monitoring", ] - + labels { foo = "bar" } - + tags = ["foo", "bar"] } } @@ -88,6 +88,9 @@ resource "google_container_cluster" "primary" { * `master_auth` - (Optional) The authentication information for accessing the Kubernetes master. Structure is documented below. +* `master_authorized_networks_config` - (Optional) The desired configuration options + for master authorized networks + * `min_master_version` - (Optional) The minimum version of the master. GKE will auto-update the master to new versions, so this does not guarantee the current master version--use the read-only `master_version` field to obtain that. @@ -154,8 +157,19 @@ The `master_auth` block supports: * `username` - (Required) The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint +The `master_authorized_networks_config` block supports: + +* `enabled` - (Required) Whether or not master authorized networks is enabled + +* `cidr_blocks` - (Optional) Defines up to 10 external networks that can access + Kubernetes master through HTTPS. To avoid upstream failures, an empty list is + passed when this feature is disabled, irrespective of values passed here. + The `node_config` block supports: +* `machine_type` - (Optional) The name of a Google Compute Engine machine type. + Defaults to `n1-standard-1`. + * `disk_size_gb` - (Optional) Size of the disk attached to each node, specified in GB. The smallest allowed disk size is 10GB. Defaults to 100GB. @@ -197,7 +211,7 @@ The `node_config` block supports: * `service_account` - (Optional) The service account to be used by the Node VMs. If not specified, the "default" service account is used. -* `tags` - (Optional) The list of instance tags applied to all nodes. Tags are used to identify +* `tags` - (Optional) The list of instance tags applied to all nodes. Tags are used to identify valid sources or targets for network firewalls. ## Attributes Reference @@ -239,4 +253,4 @@ Container clusters can be imported using the `zone`, and `name`, e.g. ``` $ terraform import google_container_cluster.mycluster us-east1-a/my-cluster -``` \ No newline at end of file +```