diff --git a/google/resource_container_cluster.go b/google/resource_container_cluster.go index a318a1f4..d9b21af3 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" @@ -134,20 +133,11 @@ func resourceContainerCluster() *schema.Resource { }, "cluster_ipv4_cidr": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - _, ipnet, err := net.ParseCIDR(value) - - if err != nil || ipnet == nil || value != ipnet.String() { - errors = append(errors, fmt.Errorf( - "%q must contain a valid CIDR", k)) - } - return - }, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validateRFC1918Network(8, 32), }, "description": { @@ -222,6 +212,35 @@ func resourceContainerCluster() *schema.Resource { }, }, + "master_authorized_networks_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cidr_blocks": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 10, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cidr_block": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.CIDRNetwork(0, 32), + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "min_master_version": { Type: schema.TypeString, Optional: true, @@ -317,6 +336,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) } @@ -482,6 +505,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) @@ -526,6 +553,29 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er d.Partial(true) + if d.HasChange("master_authorized_networks_config") { + c := d.Get("master_authorized_networks_config") + 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) @@ -816,6 +866,26 @@ func expandClusterAddonsConfig(configured interface{}) *container.AddonsConfig { return ac } +func expandMasterAuthorizedNetworksConfig(configured interface{}) *container.MasterAuthorizedNetworksConfig { + result := &container.MasterAuthorizedNetworksConfig{} + if len(configured.([]interface{})) > 0 { + result.Enabled = true + config := configured.([]interface{})[0].(map[string]interface{}) + if _, ok := config["cidr_blocks"]; ok { + cidrBlocks := config["cidr_blocks"].(*schema.Set).List() + result.CidrBlocks = make([]*container.CidrBlock, 0) + for _, v := range cidrBlocks { + cidrBlock := v.(map[string]interface{}) + result.CidrBlocks = append(result.CidrBlocks, &container.CidrBlock{ + CidrBlock: cidrBlock["cidr_block"].(string), + DisplayName: cidrBlock["display_name"].(string), + }) + } + } + } + return result +} + func flattenClusterAddonsConfig(c *container.AddonsConfig) []map[string]interface{} { result := make(map[string]interface{}) if c.HorizontalPodAutoscaling != nil { @@ -856,6 +926,21 @@ func flattenClusterNodePools(d *schema.ResourceData, config *Config, c []*contai return nodePools, nil } +func flattenMasterAuthorizedNetworksConfig(c *container.MasterAuthorizedNetworksConfig) []map[string]interface{} { + result := make(map[string]interface{}) + if c.Enabled && len(c.CidrBlocks) > 0 { + cidrBlocks := make([]map[string]interface{}, 0, len(c.CidrBlocks)) + for _, v := range c.CidrBlocks { + cidrBlocks = append(cidrBlocks, map[string]interface{}{ + "cidr_block": v.CidrBlock, + "display_name": v.DisplayName, + }) + } + result["cidr_blocks"] = cidrBlocks + } + return []map[string]interface{}{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 6931f2f4..5201b9b8 100644 --- a/google/resource_container_cluster_test.go +++ b/google/resource_container_cluster_test.go @@ -1,6 +1,7 @@ package google import ( + "bytes" "fmt" "reflect" "sort" @@ -106,6 +107,44 @@ 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, []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.cidr_blocks.#", "1"), + ), + }, + { + Config: testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName, []string{}), + Check: resource.ComposeTestCheckFunc( + testAccCheckContainerCluster("google_container_cluster.with_master_authorized_networks"), + resource.TestCheckNoResourceAttr("google_container_cluster.with_master_authorized_networks", + "master_authorized_networks_config.0.cidr_blocks"), + ), + }, + { + Config: testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName, []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.cidr_blocks.#", "1"), + ), + }, + }, + }) +} + func TestAccContainerCluster_withAdditionalZones(t *testing.T) { t.Parallel() @@ -882,6 +921,32 @@ resource "google_container_cluster" "with_master_auth" { } }`, acctest.RandString(10)) +func testAccContainerCluster_withMasterAuthorizedNetworksConfig(clusterName string, cidrs []string) string { + + cidrBlocks := "" + if len(cidrs) > 0 { + var buf bytes.Buffer + for _, c := range cidrs { + buf.WriteString(fmt.Sprintf(` + cidr_blocks { + cidr_block = "%s" + }`, c)) + } + cidrBlocks = buf.String() + } + + 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 { + %s + } +}`, clusterName, 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 1feebf1a..cf1f1ddd 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/google/validation_test.go b/google/validation_test.go index e76b954e..4b9a5c91 100644 --- a/google/validation_test.go +++ b/google/validation_test.go @@ -28,12 +28,45 @@ func TestValidateGCPName(t *testing.T) { } } +func TestValidateRFC1918Network(t *testing.T) { + x := []RFC1918NetworkTestCase{ + // No errors + {TestName: "valid 10.x", CIDR: "10.0.0.0/8", MinPrefix: 0, MaxPrefix: 32}, + {TestName: "valid 172.x", CIDR: "172.16.0.0/16", MinPrefix: 0, MaxPrefix: 32}, + {TestName: "valid 192.x", CIDR: "192.168.0.0/32", MinPrefix: 0, MaxPrefix: 32}, + {TestName: "valid, bounded 10.x CIDR", CIDR: "10.0.0.0/8", MinPrefix: 8, MaxPrefix: 32}, + {TestName: "valid, bounded 172.x CIDR", CIDR: "172.16.0.0/16", MinPrefix: 12, MaxPrefix: 32}, + {TestName: "valid, bounded 192.x CIDR", CIDR: "192.168.0.0/32", MinPrefix: 16, MaxPrefix: 32}, + + // With errors + {TestName: "empty CIDR", CIDR: "", MinPrefix: 0, MaxPrefix: 32, ExpectError: true}, + {TestName: "missing mask", CIDR: "10.0.0.0", MinPrefix: 0, MaxPrefix: 32, ExpectError: true}, + {TestName: "invalid CIDR", CIDR: "10.1.0.0/8", MinPrefix: 0, MaxPrefix: 32, ExpectError: true}, + {TestName: "valid 10.x CIDR with lower bound violation", CIDR: "10.0.0.0/8", MinPrefix: 16, MaxPrefix: 32, ExpectError: true}, + {TestName: "valid 10.x CIDR with upper bound violation", CIDR: "10.0.0.0/24", MinPrefix: 8, MaxPrefix: 16, ExpectError: true}, + {TestName: "valid public CIDR", CIDR: "8.8.8.8/32", MinPrefix: 0, MaxPrefix: 32, ExpectError: true}, + } + + es := testRFC1918Networks(x) + if len(es) > 0 { + t.Errorf("Failed to validate RFC1918 Networks: %v", es) + } +} + type GCPNameTestCase struct { TestName string Value string ExpectError bool } +type RFC1918NetworkTestCase struct { + TestName string + CIDR string + MinPrefix int + MaxPrefix int + ExpectError bool +} + func testGCPNames(cases []GCPNameTestCase) []error { es := make([]error, 0) for _, c := range cases { @@ -55,3 +88,26 @@ func testGCPName(testCase GCPNameTestCase) []error { return es } + +func testRFC1918Networks(cases []RFC1918NetworkTestCase) []error { + es := make([]error, 0) + for _, c := range cases { + es = append(es, testRFC1918Network(c)...) + } + + return es +} + +func testRFC1918Network(testCase RFC1918NetworkTestCase) []error { + f := validateRFC1918Network(testCase.MinPrefix, testCase.MaxPrefix) + _, es := f(testCase.CIDR, testCase.TestName) + if testCase.ExpectError { + if len(es) > 0 { + return nil + } + return []error{fmt.Errorf("Didn't see expected error in case \"%s\" with CIDR=\"%s\" MinPrefix=%v MaxPrefix=%v", + testCase.TestName, testCase.CIDR, testCase.MinPrefix, testCase.MaxPrefix)} + } + + return es +} diff --git a/website/docs/r/container_cluster.html.markdown b/website/docs/r/container_cluster.html.markdown index 897273c7..b002a183 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"] } } @@ -105,6 +105,10 @@ output "cluster_ca_certificate" { * `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. Omit the nested `cidr_blocks` attribute to disallow + external access (except the cluster node IPs, which GKE automatically whitelists). + * `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. @@ -171,6 +175,18 @@ 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: + +* `cidr_blocks` - (Optional) Defines up to 10 external networks that can access + Kubernetes master through HTTPS. + +The `master_authorized_networks_config.cidr_blocks` block supports: + +* `cidr_block` - (Optional) External network that can access Kubernetes master through HTTPS. + Must be specified in CIDR notation. + +* `display_name` - (Optional) Field for users to identify CIDR blocks. + The `node_config` block supports: * `disk_size_gb` - (Optional) Size of the disk attached to each node, specified @@ -214,7 +230,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