Remove switch statements between api versions for container; clean up dead version code (#1427)

* remove switch statements between api versions for container

* remove dead api version code and move things around
This commit is contained in:
Dana Hoffman 2018-05-03 21:51:54 -07:00 committed by GitHub
parent 9c5f5f63a3
commit d59fcbbc59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 253 additions and 768 deletions

View File

@ -1,242 +0,0 @@
package google
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
type ApiVersion uint8
const (
v1 ApiVersion = iota
v0beta
v1beta1
)
var OrderedComputeApiVersions = []ApiVersion{
v0beta,
v1,
}
var OrderedContainerApiVersions = []ApiVersion{
v1beta1,
v1,
}
// Convert between two types by converting to/from JSON. Intended to switch
// between multiple API versions, as they are strict supersets of one another.
// item and out are pointers to structs
func Convert(item, out interface{}) error {
bytes, err := json.Marshal(item)
if err != nil {
return err
}
err = json.Unmarshal(bytes, out)
if err != nil {
return err
}
// Converting between maps and structs only occurs when autogenerated resources convert the result
// of an HTTP request. Those results do not contain omitted fields, so no need to set them.
if _, ok := item.(map[string]interface{}); !ok {
setOmittedFields(item, out)
}
return nil
}
func setOmittedFields(item, out interface{}) {
// Both inputs must be pointers, see https://blog.golang.org/laws-of-reflection:
// "To modify a reflection object, the value must be settable."
iVal := reflect.ValueOf(item).Elem()
oVal := reflect.ValueOf(out).Elem()
// Loop through all the fields of the struct to look for omitted fields and nested fields
for i := 0; i < iVal.NumField(); i++ {
iField := iVal.Field(i)
if isEmptyValue(iField) {
continue
}
fieldInfo := iVal.Type().Field(i)
oField := oVal.FieldByName(fieldInfo.Name)
// Only look at fields that exist in the output struct
if !oField.IsValid() {
continue
}
// If the field contains a 'json:"="' tag, then it was omitted from the Marshal/Unmarshal
// call and needs to be added back in.
if fieldInfo.Tag.Get("json") == "-" {
oField.Set(iField)
}
// If this field is a struct, *struct, []struct, or []*struct, recurse.
if iField.Kind() == reflect.Struct {
setOmittedFields(iField.Addr().Interface(), oField.Addr().Interface())
}
if iField.Kind() == reflect.Ptr && iField.Type().Elem().Kind() == reflect.Struct {
setOmittedFields(iField.Interface(), oField.Interface())
}
if iField.Kind() == reflect.Slice && iField.Type().Elem().Kind() == reflect.Struct {
for j := 0; j < iField.Len(); j++ {
setOmittedFields(iField.Index(j).Addr().Interface(), oField.Index(j).Addr().Interface())
}
}
if iField.Kind() == reflect.Slice && iField.Type().Elem().Kind() == reflect.Ptr &&
iField.Type().Elem().Elem().Kind() == reflect.Struct {
for j := 0; j < iField.Len(); j++ {
setOmittedFields(iField.Index(j).Interface(), oField.Index(j).Interface())
}
}
}
}
type TerraformResourceData interface {
HasChange(string) bool
GetOk(string) (interface{}, bool)
Set(string, interface{}) error
SetId(string)
Id() string
}
// Compare the fields set in schema against a list of features and their versions to determine
// what version of the API is required in order to manage the resource.
func getApiVersion(d TerraformResourceData, resourceVersion ApiVersion, features []Feature, maxVersionFunc func(map[ApiVersion]struct{}) ApiVersion) ApiVersion {
versions := map[ApiVersion]struct{}{resourceVersion: struct{}{}}
for _, feature := range features {
if feature.InUseByDefault(d) {
versions[feature.Version] = struct{}{}
}
}
return maxVersionFunc(versions)
}
func getContainerApiVersion(d TerraformResourceData, resourceVersion ApiVersion, features []Feature) ApiVersion {
return getApiVersion(d, resourceVersion, features, maxContainerVersion)
}
// Compare the fields set in schema against a list of features and their version, and a
// list of features that exist at the base resource version that can only be update at some other
// version, to determine what version of the API is required in order to update the resource.
func getApiVersionUpdate(d TerraformResourceData, resourceVersion ApiVersion, features, updateOnlyFields []Feature, maxVersionFunc func(map[ApiVersion]struct{}) ApiVersion) ApiVersion {
versions := map[ApiVersion]struct{}{resourceVersion: struct{}{}}
for _, feature := range features {
if feature.InUseByUpdate(d) {
versions[feature.Version] = struct{}{}
}
}
for _, feature := range updateOnlyFields {
if feature.HasChangeBy(d) {
versions[feature.Version] = struct{}{}
}
}
return maxVersionFunc(versions)
}
func getContainerApiVersionUpdate(d TerraformResourceData, resourceVersion ApiVersion, features, updateOnlyFields []Feature) ApiVersion {
return getApiVersionUpdate(d, resourceVersion, features, updateOnlyFields, maxContainerVersion)
}
// A field of a resource and the version of the Compute API required to use it.
type Feature struct {
Version ApiVersion
// Path to the beta field.
//
// The feature is considered to be in-use if the field referenced by "Item" is set in the state.
// The path can reference:
// - a beta field at the top-level (e.g. "min_cpu_platform").
// - a beta field nested inside a list (e.g. "network_interface.*.alias_ip_range" is considered to be
// in-use if the "alias_ip_range" field is set in the state for any of the network interfaces).
//
// Note: beta field nested inside a SET are NOT supported at the moment.
Item string
// Optional, only set if your field has a default value.
// If the value for the field is equal to the DefaultValue, we assume the beta feature is not activated.
DefaultValue interface{}
}
// Returns true when a feature has been modified.
// This is most important when updating a resource to remove versioned feature usage; if the
// resource is reverting to its base version, it needs to perform a final update at the higher
// version in order to remove high version features.
func (s Feature) HasChangeBy(d TerraformResourceData) bool {
return d.HasChange(s.Item)
}
type InUseFunc func(d TerraformResourceData, path string, defaultValue interface{}) bool
func defaultInUseFunc(d TerraformResourceData, path string, defaultValue interface{}) bool {
// At read and delete time, there is no change.
// At create time, all fields are marked has changed. We should only consider the feature active if the field has
// a value set and that this value is not the default value.
value, ok := d.GetOk(path)
return ok && value != defaultValue
}
func updateInUseFunc(d TerraformResourceData, path string, defaultValue interface{}) bool {
// During a resource update, if the beta field has changes, the feature is considered active even if the new value
// is the default value. This is because the beta API must be called to change the value of the field back to the
// default value.
value, ok := d.GetOk(path)
return (ok && value != defaultValue) || d.HasChange(path)
}
// Return true when a feature appears in schema and doesn't hold the default value.
func (s Feature) InUseByDefault(d TerraformResourceData) bool {
return inUseBy(d, s.Item, s.DefaultValue, defaultInUseFunc)
}
func (s Feature) InUseByUpdate(d TerraformResourceData) bool {
return inUseBy(d, s.Item, s.DefaultValue, updateInUseFunc)
}
func inUseBy(d TerraformResourceData, path string, defaultValue interface{}, inUseFunc InUseFunc) bool {
pos := strings.Index(path, "*")
if pos == -1 {
return inUseFunc(d, path, defaultValue)
}
prefix := path[0:pos]
suffix := path[pos+1:]
v, ok := d.GetOk(prefix + "#")
if !ok {
return false
}
count := v.(int)
for i := 0; i < count; i++ {
nestedPath := fmt.Sprintf("%s%d%s", prefix, i, suffix)
if inUseBy(d, nestedPath, defaultValue, inUseFunc) {
return true
}
}
return false
}
func maxVersion(versionsInUse map[ApiVersion]struct{}, orderedVersions []ApiVersion) ApiVersion {
for _, version := range orderedVersions {
if _, ok := versionsInUse[version]; ok {
return version
}
}
// Fallback to the final, most stable version
return orderedVersions[len(orderedVersions)-1]
}
func maxContainerVersion(versionsInUse map[ApiVersion]struct{}) ApiVersion {
return maxVersion(versionsInUse, OrderedContainerApiVersions)
}

View File

@ -1,326 +0,0 @@
package google
import (
"reflect"
"testing"
)
type ExpectedApiVersions struct {
Create ApiVersion
ReadDelete ApiVersion
Update ApiVersion
}
func TestApiVersion(t *testing.T) {
baseVersion := v1
betaVersion := v0beta
maxTestApiVersion := func(versionsInUse map[ApiVersion]struct{}) ApiVersion {
if _, ok := versionsInUse[betaVersion]; ok {
return betaVersion
}
return baseVersion
}
cases := map[string]struct {
Features []Feature
FieldsInSchema map[string]interface{}
UpdatedFields []string
UpdateOnlyFields []Feature
ExpectedApiVersions
}{
"no beta field": {
FieldsInSchema: map[string]interface{}{
"normal_field": "foo",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: baseVersion,
},
},
"beta field not set": {
Features: []Feature{{Version: betaVersion, Item: "beta_field"}},
FieldsInSchema: map[string]interface{}{
"normal_field": "foo",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: baseVersion,
},
},
"beta field set": {
Features: []Feature{{Version: betaVersion, Item: "beta_field"}},
FieldsInSchema: map[string]interface{}{
"normal_field": "foo",
"beta_field": "bar",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: betaVersion,
ReadDelete: betaVersion,
Update: betaVersion,
},
},
"update only beta field": {
FieldsInSchema: map[string]interface{}{
"normal_field": "foo",
},
UpdatedFields: []string{"beta_update_field"},
UpdateOnlyFields: []Feature{{Version: betaVersion, Item: "beta_update_field"}},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: betaVersion,
},
},
"nested beta field not set": {
Features: []Feature{{Version: betaVersion, Item: "list_field.*.beta_nested_field"}},
FieldsInSchema: map[string]interface{}{
"list_field.#": 2,
"list_field.0.normal_field": "foo",
"list_field.1.normal_field": "bar",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: baseVersion,
},
},
"nested beta field set": {
Features: []Feature{{Version: betaVersion, Item: "list_field.*.beta_nested_field"}},
FieldsInSchema: map[string]interface{}{
"list_field.#": 2,
"list_field.0.normal_field": "foo",
"list_field.1.normal_field": "bar",
"list_field.1.beta_nested_field": "baz",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: betaVersion,
ReadDelete: betaVersion,
Update: betaVersion,
},
},
"double nested fields set": {
Features: []Feature{{Version: betaVersion, Item: "list_field.*.nested_list_field.*.beta_nested_field"}},
FieldsInSchema: map[string]interface{}{
"list_field.#": 1,
"list_field.0.nested_list_field.#": 1,
"list_field.0.nested_list_field.0.beta_nested_field": "foo",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: betaVersion,
ReadDelete: betaVersion,
Update: betaVersion,
},
},
"beta field has default value": {
Features: []Feature{{Version: betaVersion, Item: "beta_field", DefaultValue: "bar"}},
FieldsInSchema: map[string]interface{}{
"normal_field": "foo",
"beta_field": "bar",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: baseVersion,
},
},
"beta field is updated to default value": {
Features: []Feature{{Version: betaVersion, Item: "beta_field", DefaultValue: "bar"}},
FieldsInSchema: map[string]interface{}{
"normal_field": "foo",
"beta_field": "bar",
},
UpdatedFields: []string{"beta_field"},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: betaVersion,
},
},
"nested beta field has default value": {
Features: []Feature{{Version: betaVersion, Item: "list_field.*.beta_nested_field", DefaultValue: "baz"}},
FieldsInSchema: map[string]interface{}{
"list_field.#": 2,
"list_field.0.normal_field": "foo",
"list_field.1.normal_field": "bar",
"list_field.1.beta_nested_field": "baz",
},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: baseVersion,
},
},
"nested beta field is updated default value": {
Features: []Feature{{Version: betaVersion, Item: "list_field.*.beta_nested_field", DefaultValue: "baz"}},
FieldsInSchema: map[string]interface{}{
"list_field.#": 2,
"list_field.0.normal_field": "foo",
"list_field.1.normal_field": "bar",
"list_field.1.beta_nested_field": "baz",
},
UpdatedFields: []string{"list_field.1.beta_nested_field"},
ExpectedApiVersions: ExpectedApiVersions{
Create: baseVersion,
ReadDelete: baseVersion,
Update: betaVersion,
},
},
}
for tn, tc := range cases {
// Create
// All fields with value have HasChange set to true.
keys := make([]string, 0, len(tc.FieldsInSchema))
for key := range tc.FieldsInSchema {
keys = append(keys, key)
}
d := &ResourceDataMock{
FieldsInSchema: tc.FieldsInSchema,
FieldsWithHasChange: keys,
}
apiVersion := getApiVersion(d, v1, tc.Features, maxTestApiVersion)
if apiVersion != tc.ExpectedApiVersions.Create {
t.Errorf("bad: %s, Expected to see version %v for create, got version %v", tn, tc.ExpectedApiVersions.Create, apiVersion)
}
// Read/Delete
// All fields have HasChange set to false.
d = &ResourceDataMock{
FieldsInSchema: tc.FieldsInSchema,
}
apiVersion = getApiVersion(d, v1, tc.Features, maxTestApiVersion)
if apiVersion != tc.ExpectedApiVersions.ReadDelete {
t.Errorf("bad: %s, Expected to see version %v for read/delete, got version %v", tn, tc.ExpectedApiVersions.ReadDelete, apiVersion)
}
// Update
// Only fields defined as updated in the test case have HasChange set to true.
d = &ResourceDataMock{
FieldsInSchema: tc.FieldsInSchema,
FieldsWithHasChange: tc.UpdatedFields,
}
apiVersion = getApiVersionUpdate(d, v1, tc.Features, tc.UpdateOnlyFields, maxTestApiVersion)
if apiVersion != tc.ExpectedApiVersions.Update {
t.Errorf("bad: %s, Expected to see version %v for update, got version %v", tn, tc.ExpectedApiVersions.Update, apiVersion)
}
}
}
func TestSetOmittedFields(t *testing.T) {
type Inner struct {
InnerNotOmitted string `json:"notOmitted"`
InnerOmitted []string `json:"-"`
}
type InputOuter struct {
NotOmitted string `json:"notOmitted"`
Omitted []string `json:"-"`
Struct Inner
Pointer *Inner
StructSlice []Inner
PointerSlice []*Inner
Unset *Inner
OnlyInInputType *Inner
}
type OutputOuter struct {
NotOmitted string `json:"notOmitted"`
Omitted []string `json:"-"`
Struct Inner
Pointer *Inner
StructSlice []Inner
PointerSlice []*Inner
Unset *Inner
OnlyInOutputType *Inner
}
input := &InputOuter{
NotOmitted: "foo",
Omitted: []string{"foo"},
Struct: Inner{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
},
Pointer: &Inner{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
},
StructSlice: []Inner{
{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
}, {
InnerNotOmitted: "bar",
InnerOmitted: []string{"bar"},
},
},
PointerSlice: []*Inner{
{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
}, {
InnerNotOmitted: "bar",
InnerOmitted: []string{"bar"},
},
},
OnlyInInputType: &Inner{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
},
}
output := &OutputOuter{}
Convert(input, output)
if input.NotOmitted != output.NotOmitted ||
!reflect.DeepEqual(input.Omitted, output.Omitted) ||
!reflect.DeepEqual(input.Struct, output.Struct) ||
!reflect.DeepEqual(input.Pointer, output.Pointer) ||
!reflect.DeepEqual(input.StructSlice, output.StructSlice) ||
!reflect.DeepEqual(input.PointerSlice, output.PointerSlice) ||
!(input.Unset == nil && output.Unset == nil) {
t.Errorf("Structs were not equivalent after conversion:\nInput:%#v\nOutput: %#v", input, output)
}
}
type ResourceDataMock struct {
FieldsInSchema map[string]interface{}
FieldsWithHasChange []string
id string
}
func (d *ResourceDataMock) HasChange(key string) bool {
exists := false
for _, val := range d.FieldsWithHasChange {
if key == val {
exists = true
}
}
return exists
}
func (d *ResourceDataMock) GetOk(key string) (interface{}, bool) {
for k, v := range d.FieldsInSchema {
if key == k {
return v, true
}
}
return nil, false
}
func (d *ResourceDataMock) Set(key string, value interface{}) error {
d.FieldsInSchema[key] = value
return nil
}
func (d *ResourceDataMock) SetId(v string) {
d.id = v
}
func (d *ResourceDataMock) Id() string {
return d.id
}

77
google/convert.go Normal file
View File

@ -0,0 +1,77 @@
package google
import (
"encoding/json"
"reflect"
)
// Convert between two types by converting to/from JSON. Intended to switch
// between multiple API versions, as they are strict supersets of one another.
// item and out are pointers to structs
func Convert(item, out interface{}) error {
bytes, err := json.Marshal(item)
if err != nil {
return err
}
err = json.Unmarshal(bytes, out)
if err != nil {
return err
}
// Converting between maps and structs only occurs when autogenerated resources convert the result
// of an HTTP request. Those results do not contain omitted fields, so no need to set them.
if _, ok := item.(map[string]interface{}); !ok {
setOmittedFields(item, out)
}
return nil
}
func setOmittedFields(item, out interface{}) {
// Both inputs must be pointers, see https://blog.golang.org/laws-of-reflection:
// "To modify a reflection object, the value must be settable."
iVal := reflect.ValueOf(item).Elem()
oVal := reflect.ValueOf(out).Elem()
// Loop through all the fields of the struct to look for omitted fields and nested fields
for i := 0; i < iVal.NumField(); i++ {
iField := iVal.Field(i)
if isEmptyValue(iField) {
continue
}
fieldInfo := iVal.Type().Field(i)
oField := oVal.FieldByName(fieldInfo.Name)
// Only look at fields that exist in the output struct
if !oField.IsValid() {
continue
}
// If the field contains a 'json:"="' tag, then it was omitted from the Marshal/Unmarshal
// call and needs to be added back in.
if fieldInfo.Tag.Get("json") == "-" {
oField.Set(iField)
}
// If this field is a struct, *struct, []struct, or []*struct, recurse.
if iField.Kind() == reflect.Struct {
setOmittedFields(iField.Addr().Interface(), oField.Addr().Interface())
}
if iField.Kind() == reflect.Ptr && iField.Type().Elem().Kind() == reflect.Struct {
setOmittedFields(iField.Interface(), oField.Interface())
}
if iField.Kind() == reflect.Slice && iField.Type().Elem().Kind() == reflect.Struct {
for j := 0; j < iField.Len(); j++ {
setOmittedFields(iField.Index(j).Addr().Interface(), oField.Index(j).Addr().Interface())
}
}
if iField.Kind() == reflect.Slice && iField.Type().Elem().Kind() == reflect.Ptr &&
iField.Type().Elem().Elem().Kind() == reflect.Struct {
for j := 0; j < iField.Len(); j++ {
setOmittedFields(iField.Index(j).Interface(), oField.Index(j).Interface())
}
}
}
}

79
google/convert_test.go Normal file
View File

@ -0,0 +1,79 @@
package google
import (
"reflect"
"testing"
)
func TestSetOmittedFields(t *testing.T) {
type Inner struct {
InnerNotOmitted string `json:"notOmitted"`
InnerOmitted []string `json:"-"`
}
type InputOuter struct {
NotOmitted string `json:"notOmitted"`
Omitted []string `json:"-"`
Struct Inner
Pointer *Inner
StructSlice []Inner
PointerSlice []*Inner
Unset *Inner
OnlyInInputType *Inner
}
type OutputOuter struct {
NotOmitted string `json:"notOmitted"`
Omitted []string `json:"-"`
Struct Inner
Pointer *Inner
StructSlice []Inner
PointerSlice []*Inner
Unset *Inner
OnlyInOutputType *Inner
}
input := &InputOuter{
NotOmitted: "foo",
Omitted: []string{"foo"},
Struct: Inner{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
},
Pointer: &Inner{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
},
StructSlice: []Inner{
{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
}, {
InnerNotOmitted: "bar",
InnerOmitted: []string{"bar"},
},
},
PointerSlice: []*Inner{
{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
}, {
InnerNotOmitted: "bar",
InnerOmitted: []string{"bar"},
},
},
OnlyInInputType: &Inner{
InnerNotOmitted: "foo",
InnerOmitted: []string{"foo"},
},
}
output := &OutputOuter{}
Convert(input, output)
if input.NotOmitted != output.NotOmitted ||
!reflect.DeepEqual(input.Omitted, output.Omitted) ||
!reflect.DeepEqual(input.Struct, output.Struct) ||
!reflect.DeepEqual(input.Pointer, output.Pointer) ||
!reflect.DeepEqual(input.StructSlice, output.StructSlice) ||
!reflect.DeepEqual(input.PointerSlice, output.PointerSlice) ||
!(input.Unset == nil && output.Unset == nil) {
t.Errorf("Structs were not equivalent after conversion:\nInput:%#v\nOutput: %#v", input, output)
}
}

View File

@ -8,9 +8,6 @@ import (
"google.golang.org/api/compute/v1"
)
var ProjectMetadataBaseApiVersion = v1
var ProjectMetadataVersionedFeatures = []Feature{}
func resourceComputeProjectMetadata() *schema.Resource {
return &schema.Resource{
Create: resourceComputeProjectMetadataCreate,

View File

@ -12,23 +12,12 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"google.golang.org/api/container/v1"
containerBeta "google.golang.org/api/container/v1beta1"
)
var (
instanceGroupManagerURL = regexp.MustCompile(fmt.Sprintf("^https://www.googleapis.com/compute/v1/projects/(%s)/zones/([a-z0-9-]*)/instanceGroupManagers/([^/]*)", ProjectRegex))
ContainerClusterBaseApiVersion = v1
ContainerClusterVersionedFeatures = []Feature{
{Version: v1beta1, Item: "pod_security_policy_config"},
{Version: v1beta1, Item: "node_config.*.taint"},
{Version: v1beta1, Item: "node_config.*.workload_metadata_config"},
{Version: v1beta1, Item: "private_cluster"},
{Version: v1beta1, Item: "master_ipv4_cidr_block"},
{Version: v1beta1, Item: "region"},
}
networkConfig = &schema.Resource{
Schema: map[string]*schema.Schema{
"cidr_blocks": {
@ -475,7 +464,6 @@ func resourceContainerCluster() *schema.Resource {
}
func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) error {
containerAPIVersion := getContainerApiVersion(d, ContainerClusterBaseApiVersion, ContainerClusterVersionedFeatures)
config := meta.(*Config)
project, err := getProject(d, config)
@ -651,25 +639,8 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er
mutexKV.Lock(containerClusterMutexKey(project, location, clusterName))
defer mutexKV.Unlock(containerClusterMutexKey(project, location, clusterName))
var op interface{}
switch containerAPIVersion {
case v1:
reqV1 := &container.CreateClusterRequest{}
err = Convert(req, reqV1)
if err != nil {
return err
}
op, err = config.clientContainer.Projects.Zones.Clusters.Create(project, location, reqV1).Do()
case v1beta1:
reqV1Beta := &containerBeta.CreateClusterRequest{}
err = Convert(req, reqV1Beta)
if err != nil {
return err
}
parent := fmt.Sprintf("projects/%s/locations/%s", project, location)
op, err = config.clientContainerBeta.Projects.Locations.Clusters.Create(parent, reqV1Beta).Do()
}
parent := fmt.Sprintf("projects/%s/locations/%s", project, location)
op, err := config.clientContainerBeta.Projects.Locations.Clusters.Create(parent, req).Do()
if err != nil {
return err
}
@ -687,15 +658,8 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er
log.Printf("[INFO] GKE cluster %s has been created", clusterName)
if d.Get("remove_default_node_pool").(bool) {
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.NodePools.Delete(
project, location, clusterName, "default-pool").Do()
case v1beta1:
parent := fmt.Sprintf("%s/nodePools/%s", containerClusterFullName(project, location, clusterName), "default-pool")
op, err = config.clientContainerBeta.Projects.Locations.Clusters.NodePools.Delete(parent).Do()
}
parent := fmt.Sprintf("%s/nodePools/%s", containerClusterFullName(project, location, clusterName), "default-pool")
op, err = config.clientContainerBeta.Projects.Locations.Clusters.NodePools.Delete(parent).Do()
if err != nil {
return errwrap.Wrapf("Error deleting default node pool: {{err}}", err)
}
@ -709,7 +673,6 @@ func resourceContainerClusterCreate(d *schema.ResourceData, meta interface{}) er
}
func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) error {
containerAPIVersion := getContainerApiVersion(d, ContainerClusterBaseApiVersion, ContainerClusterVersionedFeatures)
config := meta.(*Config)
project, err := getProject(d, config)
@ -723,20 +686,9 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro
}
cluster := &containerBeta.Cluster{}
var clust interface{}
err = resource.Retry(2*time.Minute, func() *resource.RetryError {
switch containerAPIVersion {
case v1:
clust, err = config.clientContainer.Projects.Zones.Clusters.Get(
project, location, d.Get("name").(string)).Do()
case v1beta1:
name := containerClusterFullName(project, location, d.Get("name").(string))
clust, err = config.clientContainerBeta.Projects.Locations.Clusters.Get(name).Do()
}
if err != nil {
return resource.NonRetryableError(err)
}
err = Convert(clust, cluster)
name := containerClusterFullName(project, location, d.Get("name").(string))
cluster, err = config.clientContainerBeta.Projects.Locations.Clusters.Get(name).Do()
if err != nil {
return resource.NonRetryableError(err)
}
@ -834,7 +786,6 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro
}
func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) error {
containerAPIVersion := getContainerApiVersion(d, ContainerClusterBaseApiVersion, ContainerClusterVersionedFeatures)
config := meta.(*Config)
project, err := getProject(d, config)
@ -854,22 +805,10 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
lockKey := containerClusterMutexKey(project, location, clusterName)
updateFunc := func(req *container.UpdateClusterRequest, updateDescription string) func() error {
updateFunc := func(req *containerBeta.UpdateClusterRequest, updateDescription string) func() error {
return func() error {
var err error
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.Update(project, location, clusterName, req).Do()
case v1beta1:
reqV1Beta := &containerBeta.UpdateClusterRequest{}
err = Convert(req, reqV1Beta)
if err != nil {
return err
}
name := containerClusterFullName(project, location, clusterName)
op, err = config.clientContainerBeta.Projects.Locations.Clusters.Update(name, reqV1Beta).Do()
}
name := containerClusterFullName(project, location, clusterName)
op, err := config.clientContainerBeta.Projects.Locations.Clusters.Update(name, req).Do()
if err != nil {
return err
}
@ -883,14 +822,9 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
// if the order of updating fields does matter, it is called out explicitly.
if d.HasChange("master_authorized_networks_config") {
c := d.Get("master_authorized_networks_config")
conf := &container.MasterAuthorizedNetworksConfig{}
err := Convert(expandMasterAuthorizedNetworksConfig(c), conf)
if err != nil {
return err
}
req := &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
DesiredMasterAuthorizedNetworksConfig: conf,
req := &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredMasterAuthorizedNetworksConfig: expandMasterAuthorizedNetworksConfig(c),
},
}
@ -918,8 +852,8 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
// Only upgrade the master if the current version is lower than the desired version
if cur.LessThan(des) {
req := &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
req := &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredMasterVersion: desiredMasterVersion,
},
}
@ -936,8 +870,8 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
if d.HasChange("node_version") {
desiredNodeVersion := d.Get("node_version").(string)
req := &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
req := &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredNodeVersion: desiredNodeVersion,
},
}
@ -955,14 +889,9 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
if d.HasChange("addons_config") {
if ac, ok := d.GetOk("addons_config"); ok {
conf := &container.AddonsConfig{}
err := Convert(expandClusterAddonsConfig(ac), conf)
if err != nil {
return err
}
req := &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
DesiredAddonsConfig: conf,
req := &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredAddonsConfig: expandClusterAddonsConfig(ac),
},
}
@ -979,37 +908,20 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
}
if d.HasChange("maintenance_policy") {
var req *container.SetMaintenancePolicyRequest
var req *containerBeta.SetMaintenancePolicyRequest
if mp, ok := d.GetOk("maintenance_policy"); ok {
pol := &container.MaintenancePolicy{}
err := Convert(expandMaintenancePolicy(mp), pol)
if err != nil {
return err
}
req = &container.SetMaintenancePolicyRequest{
MaintenancePolicy: pol,
req = &containerBeta.SetMaintenancePolicyRequest{
MaintenancePolicy: expandMaintenancePolicy(mp),
}
} else {
req = &container.SetMaintenancePolicyRequest{
req = &containerBeta.SetMaintenancePolicyRequest{
NullFields: []string{"MaintenancePolicy"},
}
}
updateF := func() error {
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.SetMaintenancePolicy(
project, location, clusterName, req).Do()
case v1beta1:
reqV1Beta := &containerBeta.SetMaintenancePolicyRequest{}
err = Convert(req, reqV1Beta)
if err != nil {
return err
}
name := containerClusterFullName(project, location, clusterName)
op, err = config.clientContainerBeta.Projects.Locations.Clusters.SetMaintenancePolicy(name, reqV1Beta).Do()
}
name := containerClusterFullName(project, location, clusterName)
op, err := config.clientContainerBeta.Projects.Locations.Clusters.SetMaintenancePolicy(name, req).Do()
if err != nil {
return err
@ -1044,8 +956,8 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
azSet.Add(location)
}
req := &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
req := &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredLocations: convertStringSet(azSet),
},
}
@ -1060,8 +972,8 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
azSetNew.Add(location)
}
if !azSet.Equal(azSetNew) {
req = &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
req = &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredLocations: convertStringSet(azSetNew),
},
}
@ -1080,26 +992,15 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
if d.HasChange("enable_legacy_abac") {
enabled := d.Get("enable_legacy_abac").(bool)
req := &container.SetLegacyAbacRequest{
req := &containerBeta.SetLegacyAbacRequest{
Enabled: enabled,
ForceSendFields: []string{"Enabled"},
}
updateF := func() error {
log.Println("[DEBUG] updating enable_legacy_abac")
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.LegacyAbac(project, location, clusterName, req).Do()
case v1beta1:
reqV1Beta := &containerBeta.SetLegacyAbacRequest{}
err = Convert(req, reqV1Beta)
if err != nil {
return err
}
name := containerClusterFullName(project, location, clusterName)
op, err = config.clientContainerBeta.Projects.Locations.Clusters.SetLegacyAbac(name, reqV1Beta).Do()
}
name := containerClusterFullName(project, location, clusterName)
op, err := config.clientContainerBeta.Projects.Locations.Clusters.SetLegacyAbac(name, req).Do()
if err != nil {
return err
}
@ -1123,8 +1024,8 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
if d.HasChange("monitoring_service") {
desiredMonitoringService := d.Get("monitoring_service").(string)
req := &container.UpdateClusterRequest{
Update: &container.ClusterUpdate{
req := &containerBeta.UpdateClusterRequest{
Update: &containerBeta.ClusterUpdate{
DesiredMonitoringService: desiredMonitoringService,
},
}
@ -1142,32 +1043,14 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
if d.HasChange("network_policy") {
np := d.Get("network_policy")
pol := &container.NetworkPolicy{}
err := Convert(expandNetworkPolicy(np), pol)
if err != nil {
return err
}
req := &container.SetNetworkPolicyRequest{
NetworkPolicy: pol,
req := &containerBeta.SetNetworkPolicyRequest{
NetworkPolicy: expandNetworkPolicy(np),
}
updateF := func() error {
log.Println("[DEBUG] updating network_policy")
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.SetNetworkPolicy(
project, location, clusterName, req).Do()
case v1beta1:
reqV1Beta := &containerBeta.SetNetworkPolicyRequest{}
err = Convert(req, reqV1Beta)
if err != nil {
return err
}
name := containerClusterFullName(project, location, clusterName)
op, err = config.clientContainerBeta.Projects.Locations.Clusters.SetNetworkPolicy(name, reqV1Beta).Do()
}
name := containerClusterFullName(project, location, clusterName)
op, err := config.clientContainerBeta.Projects.Locations.Clusters.SetNetworkPolicy(name, req).Do()
if err != nil {
return err
}
@ -1206,24 +1089,12 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
if d.HasChange("logging_service") {
logging := d.Get("logging_service").(string)
req := &container.SetLoggingServiceRequest{
req := &containerBeta.SetLoggingServiceRequest{
LoggingService: logging,
}
updateF := func() error {
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.Logging(
project, location, clusterName, req).Do()
case v1beta1:
reqV1Beta := &containerBeta.SetLoggingServiceRequest{}
err = Convert(req, reqV1Beta)
if err != nil {
return err
}
name := containerClusterFullName(project, location, clusterName)
op, err = config.clientContainerBeta.Projects.Locations.Clusters.SetLogging(name, reqV1Beta).Do()
}
name := containerClusterFullName(project, location, clusterName)
op, err := config.clientContainerBeta.Projects.Locations.Clusters.SetLogging(name, req).Do()
if err != nil {
return err
}
@ -1267,15 +1138,8 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er
}
if d.HasChange("remove_default_node_pool") && d.Get("remove_default_node_pool").(bool) {
var op interface{}
switch containerAPIVersion {
case v1:
op, err = config.clientContainer.Projects.Zones.Clusters.NodePools.Delete(
project, location, clusterName, "default-pool").Do()
case v1beta1:
name := fmt.Sprintf("%s/nodePools/%s", containerClusterFullName(project, location, clusterName), "default-pool")
op, err = config.clientContainerBeta.Projects.Locations.Clusters.NodePools.Delete(name).Do()
}
name := fmt.Sprintf("%s/nodePools/%s", containerClusterFullName(project, location, clusterName), "default-pool")
op, err := config.clientContainerBeta.Projects.Locations.Clusters.NodePools.Delete(name).Do()
if err != nil {
return errwrap.Wrapf("Error deleting default node pool: {{err}}", err)
}

View File

@ -431,9 +431,6 @@ func TestAccContainerCluster_withPrivateCluster(t *testing.T) {
ImportStateIdPrefix: "us-central1-a/",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"private_cluster",
"master_ipv4_cidr_block"},
},
},
})
@ -642,17 +639,11 @@ func TestAccContainerCluster_withWorkloadMetadataConfig(t *testing.T) {
),
},
{
ResourceName: "google_container_cluster.with_workload_metadata_config",
ImportStateIdPrefix: "us-central1-a/",
ImportState: true,
ImportStateVerify: true,
// Import always uses the v1 API, so beta features don't get imported.
ImportStateVerifyIgnore: []string{
"node_config.0.workload_metadata_config.#",
"node_config.0.workload_metadata_config.0.node_metadata",
"node_pool.0.node_config.0.workload_metadata_config.#",
"node_pool.0.node_config.0.workload_metadata_config.0.node_metadata",
"min_master_version"},
ResourceName: "google_container_cluster.with_workload_metadata_config",
ImportStateIdPrefix: "us-central1-a/",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"min_master_version"},
},
},
})
@ -1125,8 +1116,6 @@ func TestAccContainerCluster_withPodSecurityPolicy(t *testing.T) {
ImportStateIdPrefix: "us-central1-a/",
ImportState: true,
ImportStateVerify: true,
// Import always uses the v1 API, so beta features don't get imported.
ImportStateVerifyIgnore: []string{"pod_security_policy_config.#", "pod_security_policy_config.0.enabled"},
},
{
Config: testAccContainerCluster_withPodSecurityPolicy(clusterName, false),
@ -1140,8 +1129,6 @@ func TestAccContainerCluster_withPodSecurityPolicy(t *testing.T) {
ImportStateIdPrefix: "us-central1-a/",
ImportState: true,
ImportStateVerify: true,
// Import always uses the v1 API, so beta features don't get imported.
ImportStateVerifyIgnore: []string{"pod_security_policy_config.#", "pod_security_policy_config.0.enabled"},
},
},
})

41
google/test_utils.go Normal file
View File

@ -0,0 +1,41 @@
package google
type ResourceDataMock struct {
FieldsInSchema map[string]interface{}
FieldsWithHasChange []string
id string
}
func (d *ResourceDataMock) HasChange(key string) bool {
exists := false
for _, val := range d.FieldsWithHasChange {
if key == val {
exists = true
}
}
return exists
}
func (d *ResourceDataMock) GetOk(key string) (interface{}, bool) {
for k, v := range d.FieldsInSchema {
if key == k {
return v, true
}
}
return nil, false
}
func (d *ResourceDataMock) Set(key string, value interface{}) error {
d.FieldsInSchema[key] = value
return nil
}
func (d *ResourceDataMock) SetId(v string) {
d.id = v
}
func (d *ResourceDataMock) Id() string {
return d.id
}

View File

@ -17,6 +17,14 @@ import (
"google.golang.org/api/googleapi"
)
type TerraformResourceData interface {
HasChange(string) bool
GetOk(string) (interface{}, bool)
Set(string, interface{}) error
SetId(string)
Id() string
}
// getRegionFromZone returns the region from a zone for Google cloud.
func getRegionFromZone(zone string) string {
if zone != "" && len(zone) > 2 {