Add support for master authorized networks in google_container_cluster (#626)

* Add support for master authorized networks in `google_container_cluster`

* [review] remove enabled flag / restructure schema

- remove `google_container_cluster.master_authorized_networks_config.enabled`
- add `display_name` and restructure schema as follows:
    master_authorized_networks_config {
        cidr_blocks {
            cidr_block   = "0.0.0.0/0"
            display_name = "foo"
        }
    }
- amend tests

* [review] add test for validateRFC1918Network, fix acc test
This commit is contained in:
David Quarles 2017-11-02 10:38:20 -07:00 committed by Dana Hoffman
parent 8cf605f23e
commit ca7551c8c5
5 changed files with 270 additions and 18 deletions

View File

@ -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 {

View File

@ -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" {

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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