Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for maintenance window on google_container_cluster #670

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions google/resource_container_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,36 @@ func resourceContainerCluster() *schema.Resource {
ValidateFunc: validation.StringInSlice([]string{"logging.googleapis.com", "none"}, false),
},

"maintenance_policy": {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"daily_maintenance_window": {
Type: schema.TypeList,
Required: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the sake of future-proofing, let's go ahead and ForceNew: true this one too

MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"start_time": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateRFC3339Time,
},
"duration": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
},
},

"master_auth": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -327,6 +357,19 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er

timeoutInMinutes := int(d.Timeout(schema.TimeoutCreate).Minutes())

if v, ok := d.GetOk("maintenance_policy"); ok {
maintenancePolicy := v.([]interface{})[0].(map[string]interface{})
dailyMaintenanceWindow := maintenancePolicy["daily_maintenance_window"].([]interface{})[0].(map[string]interface{})
startTime := dailyMaintenanceWindow["start_time"].(string)
cluster.MaintenancePolicy = &container.MaintenancePolicy{
Window: &container.MaintenanceWindow{
DailyMaintenanceWindow: &container.DailyMaintenanceWindow{
StartTime: startTime,
},
},
}
}

if v, ok := d.GetOk("master_auth"); ok {
masterAuths := v.([]interface{})
masterAuth := masterAuths[0].(map[string]interface{})
Expand Down Expand Up @@ -494,6 +537,20 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro

d.Set("endpoint", cluster.Endpoint)

if cluster.MaintenancePolicy != nil && cluster.MaintenancePolicy.Window != nil && cluster.MaintenancePolicy.Window.DailyMaintenanceWindow != nil {
maintenancePolicy := []map[string]interface{}{
{
"daily_maintenance_window": []map[string]interface{}{
{
"start_time": cluster.MaintenancePolicy.Window.DailyMaintenanceWindow.StartTime,
"duration": cluster.MaintenancePolicy.Window.DailyMaintenanceWindow.Duration,
},
},
},
}
d.Set("maintenance_policy", maintenancePolicy)
}

masterAuth := []map[string]interface{}{
{
"username": cluster.MasterAuth.Username,
Expand Down
39 changes: 39 additions & 0 deletions google/resource_container_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,25 @@ func TestAccContainerCluster_withNodePoolNodeConfig(t *testing.T) {
})
}

func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckContainerClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccContainerCluster_withMaintenanceWindow("03:00"),
Check: resource.ComposeTestCheckFunc(
testAccCheckContainerCluster(
"google_container_cluster.primary"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be "google_container_cluster.with_maintenance_window" to match the resource name in the test config

),
},
},
})
}

func testAccCheckContainerClusterDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)

Expand Down Expand Up @@ -685,6 +704,11 @@ func testAccCheckContainerCluster(n string) resource.TestCheckFunc {
clusterTests = append(clusterTests, clusterTestField{"addons_config.0.horizontal_pod_autoscaling.0.disabled", horizontalPodAutoscalingDisabled})
clusterTests = append(clusterTests, clusterTestField{"addons_config.0.kubernetes_dashboard.0.disabled", kubernetesDashboardDisabled})

if cluster.MaintenancePolicy != nil {
clusterTests = append(clusterTests, clusterTestField{"maintenance_policy.0.daily_maintenance_window.0.start_time", cluster.MaintenancePolicy.Window.DailyMaintenanceWindow.StartTime})
clusterTests = append(clusterTests, clusterTestField{"maintenance_policy.0.daily_maintenance_window.0.duration", cluster.MaintenancePolicy.Window.DailyMaintenanceWindow.Duration})
}

for i, np := range cluster.NodePools {
prefix := fmt.Sprintf("node_pool.%d.", i)
clusterTests = append(clusterTests, clusterTestField{prefix + "name", np.Name})
Expand Down Expand Up @@ -1441,3 +1465,18 @@ resource "google_container_cluster" "with_node_pool_node_config" {
}
`, testId, testId)
}

func testAccContainerCluster_withMaintenanceWindow(startTime string) string {
return fmt.Sprintf(`
resource "google_container_cluster" "with_maintenance_window" {
name = "cluster-test-%s"
zone = "us-central1-a"
initial_node_count = 1

maintenance_policy {
daily_maintenance_window {
start_time = "%s"
}
}
}`, acctest.RandString(10), startTime)
}
18 changes: 18 additions & 0 deletions google/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform/helper/validation"
"net"
"regexp"
"strconv"
)

const (
Expand Down Expand Up @@ -61,3 +62,20 @@ func validateRFC1918Network(min, max int) schema.SchemaValidateFunc {
return
}
}

func validateRFC3339Time(v interface{}, k string) (warnings []string, errors []error) {
time := v.(string)
if len(time) != 5 || time[2] != ':' {
errors = append(errors, fmt.Errorf("%q (%q) must be in the format HH:mm (RFC3399)", k, time))
return
}
if hour, err := strconv.ParseUint(time[:2], 10, 0); err != nil || hour > 23 {
errors = append(errors, fmt.Errorf("%q (%q) does not contain a valid hour (00-23)", k, time))
return
}
if min, err := strconv.ParseUint(time[3:], 10, 0); err != nil || min > 59 {
errors = append(errors, fmt.Errorf("%q (%q) does not contain a valid minute (00-59)", k, time))
return
}
return
}
35 changes: 28 additions & 7 deletions google/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package google

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"testing"
)

func TestValidateGCPName(t *testing.T) {
x := []GCPNameTestCase{
x := []StringValidationTestCase{
// No errors
{TestName: "basic", Value: "foobar"},
{TestName: "with numbers", Value: "foobar123"},
Expand All @@ -22,7 +23,7 @@ func TestValidateGCPName(t *testing.T) {
{TestName: "too long", Value: "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob", ExpectError: true},
}

es := testGCPNames(x)
es := testStringValidationCases(x, validateGCPName)
if len(es) > 0 {
t.Errorf("Failed to validate GCP names: %v", es)
}
Expand Down Expand Up @@ -53,7 +54,27 @@ func TestValidateRFC1918Network(t *testing.T) {
}
}

type GCPNameTestCase struct {
func TestValidateRFC3339Time(t *testing.T) {
cases := []StringValidationTestCase{
// No errors
{TestName: "midnight", Value: "00:00"},
{TestName: "one minute before midnight", Value: "23:59"},

// With errors
{TestName: "single-digit hour", Value: "3:00", ExpectError: true},
{TestName: "hour out of range", Value: "24:00", ExpectError: true},
{TestName: "minute out of range", Value: "03:60", ExpectError: true},
{TestName: "missing colon", Value: "0100", ExpectError: true},
{TestName: "not numbers", Value: "ab:cd", ExpectError: true},
}

es := testStringValidationCases(cases, validateRFC3339Time)
if len(es) > 0 {
t.Errorf("Failed to validate RFC3339 times: %v", es)
}
}

type StringValidationTestCase struct {
TestName string
Value string
ExpectError bool
Expand All @@ -67,17 +88,17 @@ type RFC1918NetworkTestCase struct {
ExpectError bool
}

func testGCPNames(cases []GCPNameTestCase) []error {
func testStringValidationCases(cases []StringValidationTestCase, validationFunc schema.SchemaValidateFunc) []error {
es := make([]error, 0)
for _, c := range cases {
es = append(es, testGCPName(c)...)
es = append(es, testStringValidation(c, validationFunc)...)
}

return es
}

func testGCPName(testCase GCPNameTestCase) []error {
_, es := validateGCPName(testCase.Value, testCase.TestName)
func testStringValidation(testCase StringValidationTestCase, validationFunc schema.SchemaValidateFunc) []error {
_, es := validationFunc(testCase.Value, testCase.TestName)
if testCase.ExpectError {
if len(es) > 0 {
return nil
Expand Down
21 changes: 21 additions & 0 deletions website/docs/r/container_cluster.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ output "cluster_ca_certificate" {
write logs to. Available options include `logging.googleapis.com` and
`none`. Defaults to `logging.googleapis.com`

* `maintenance_policy` - (Optional) The maintenance policy to use for the cluster. Structure is
documented below.

* `master_auth` - (Optional) The authentication information for accessing the
Kubernetes master. Structure is documented below.

Expand Down Expand Up @@ -167,6 +170,20 @@ addons_config {
}
```

The `maintenance_policy` block supports:

* `daily_maintenance_window` - (Required) Time window specified for daily maintenance operations.
Specify `start_time` in [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) format "HH:MM”,
where HH : \[00-23\] and MM : \[00-59\] GMT. For example:

```
maintenance_policy {
daily_maintenance_window {
start_time = "03:00"
}
}
```

The `master_auth` block supports:

* `password` - (Required) The password to use for HTTP basic authentication when accessing
Expand Down Expand Up @@ -243,6 +260,10 @@ exported:
* `instance_group_urls` - List of instance group URLs which have been assigned
to the cluster.

* `maintenance_policy.daily_maintenance_window.duration` - Duration of the time window, automatically chosen to be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested fields are fun! This is actually maintenance_policy.0.daily_maintenance_window.0.duration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes - I see this was recently updated for the master_auth stuff (this explains why it didn't work when I tried to use those attributes without the 0 index recently!)

smallest possible in the given scenario.
Duration will be in [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) format "PTnHnMnS".

* `master_auth.0.client_certificate` - Base64 encoded public certificate
used by clients to authenticate to the cluster endpoint.

Expand Down