Add support for maintenance window on google_container_cluster (#670)

* Add support for maintenance window on google_container_cluster (#526)

* Address review comments

- Set ForceNew: true on the schema element daily_maintenance_window
- Correct resource name in acceptance test
- Correct documentation of resource attribute maintenance_policy.0.daily_maintenance_window.0.duration
This commit is contained in:
Michael Bannister 2017-11-07 23:42:11 +00:00 committed by Dana Hoffman
parent 102c2127ef
commit 12060f9f3d
5 changed files with 164 additions and 7 deletions

View File

@ -172,6 +172,37 @@ 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,
ForceNew: true,
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,
@ -327,6 +358,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{})
@ -494,6 +538,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,

View File

@ -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.with_maintenance_window"),
),
},
},
})
}
func testAccCheckContainerClusterDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
@ -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})
@ -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)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/hashicorp/terraform/helper/validation"
"net"
"regexp"
"strconv"
)
const (
@ -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
}

View File

@ -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"},
@ -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)
}
@ -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
@ -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

View File

@ -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.
@ -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
@ -243,6 +260,10 @@ exported:
* `instance_group_urls` - List of instance group URLs which have been assigned
to the cluster.
* `maintenance_policy.0.daily_maintenance_window.0.duration` - Duration of the time window, automatically chosen to be
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.