Skip to content

Commit

Permalink
Add versioned Beta support to google_compute_instance_group_manager (h…
Browse files Browse the repository at this point in the history
…ashicorp#234)

* Vendor GCP Compute Beta client library.

* Refactor resource_compute_instance_group_manager for multi version support (hashicorp#129)

* Refactor resource_compute_instance_group_manager for multi version support.
* Minor changes based on review.
* Removed type-specific API version conversion functions.

* Add support for Beta operations.

* Add v0beta support to google_compute_instance_group_manager.

* Renamed Key to Feature, added comments & updated some parameter names.

* Fix code and tests for version finder to match fields that don't have a change.

* Store non-v1 resources' self links as v1 so that dependent single-version resources don't see diffs.

* Fix weird change to vendor.json from merge.

* Add a note that Convert loses ForceSendFields, fix failing test.

* Moved nil type to a switch case in compute_shared_operation.go.

* Move base api version declaration above schema.
  • Loading branch information
rileykarson authored and Dmitry Vlasov committed Aug 15, 2017
1 parent 081d697 commit 30b7073
Show file tree
Hide file tree
Showing 9 changed files with 799 additions and 78 deletions.
100 changes: 100 additions & 0 deletions google/api_versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package google

import (
"encoding/json"
)

type ComputeApiVersion uint8

const (
v1 ComputeApiVersion = iota
v0beta
)

var OrderedComputeApiVersions = []ComputeApiVersion{
v0beta,
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.
// Convert loses information about ForceSendFields and NullFields.
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
}

return nil
}

type TerraformResourceData interface {
HasChange(string) bool
GetOk(string) (interface{}, bool)
}

// 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 getComputeApiVersion(d TerraformResourceData, resourceVersion ComputeApiVersion, features []Feature) ComputeApiVersion {
versions := map[ComputeApiVersion]struct{}{resourceVersion: struct{}{}}
for _, feature := range features {
if feature.InUseBy(d) {
versions[feature.Version] = struct{}{}
}
}

return maxVersion(versions)
}

// 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 getComputeApiVersionUpdate(d TerraformResourceData, resourceVersion ComputeApiVersion, features, updateOnlyFields []Feature) ComputeApiVersion {
versions := map[ComputeApiVersion]struct{}{resourceVersion: struct{}{}}
schemaVersion := getComputeApiVersion(d, resourceVersion, features)
versions[schemaVersion] = struct{}{}

for _, feature := range updateOnlyFields {
if feature.HasChangeBy(d) {
versions[feature.Version] = struct{}{}
}
}

return maxVersion(versions)
}

// A field of a resource and the version of the Compute API required to use it.
type Feature struct {
Version ComputeApiVersion
Item string
}

// 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)
}

// Return true when a feature appears in schema or has been modified.
func (s Feature) InUseBy(d TerraformResourceData) bool {
_, ok := d.GetOk(s.Item)
return ok || s.HasChangeBy(d)
}

func maxVersion(versionsInUse map[ComputeApiVersion]struct{}) ComputeApiVersion {
for _, version := range OrderedComputeApiVersions {
if _, ok := versionsInUse[version]; ok {
return version
}
}

// Fallback to the final, most stable version
return OrderedComputeApiVersions[len(OrderedComputeApiVersions)-1]
}
105 changes: 105 additions & 0 deletions google/api_versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package google

import "testing"

func TestResourceWithOnlyBaseVersionFields(t *testing.T) {
d := &ResourceDataMock{
FieldsInSchema: []string{"normal_field"},
}

resourceVersion := v1
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{})
if computeApiVersion != resourceVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", resourceVersion, computeApiVersion)
}

computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{}, []Feature{})
if computeApiVersion != resourceVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", resourceVersion, computeApiVersion)
}
}

func TestResourceWithBetaFields(t *testing.T) {
resourceVersion := v1
d := &ResourceDataMock{
FieldsInSchema: []string{"normal_field", "beta_field"},
}

expectedVersion := v0beta
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}})
if computeApiVersion != expectedVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
}

computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}, []Feature{})
if computeApiVersion != expectedVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
}
}

func TestResourceWithBetaFieldsNotInSchema(t *testing.T) {
resourceVersion := v1
d := &ResourceDataMock{
FieldsInSchema: []string{"normal_field"},
}

expectedVersion := v1
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}})
if computeApiVersion != expectedVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
}

computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}, []Feature{})
if computeApiVersion != expectedVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
}
}

func TestResourceWithBetaUpdateFields(t *testing.T) {
resourceVersion := v1
d := &ResourceDataMock{
FieldsInSchema: []string{"normal_field", "beta_update_field"},
FieldsWithHasChange: []string{"beta_update_field"},
}

expectedVersion := v1
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{})
if computeApiVersion != expectedVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
}

expectedVersion = v0beta
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{}, []Feature{{Version: expectedVersion, Item: "beta_update_field"}})
if computeApiVersion != expectedVersion {
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
}

}

type ResourceDataMock struct {
FieldsInSchema []string
FieldsWithHasChange []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) {
exists := false
for _, val := range d.FieldsInSchema {
if key == val {
exists = true
}

}

return nil, exists
}
167 changes: 167 additions & 0 deletions google/compute_beta_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package google

import (
"bytes"
"fmt"
"log"
"time"

"github.com/hashicorp/terraform/helper/resource"

computeBeta "google.golang.org/api/compute/v0.beta"
)

// OperationBetaWaitType is an enum specifying what type of operation
// we're waiting on from the beta API.
type ComputeBetaOperationWaitType byte

const (
ComputeBetaOperationWaitInvalid ComputeBetaOperationWaitType = iota
ComputeBetaOperationWaitGlobal
ComputeBetaOperationWaitRegion
ComputeBetaOperationWaitZone
)

type ComputeBetaOperationWaiter struct {
Service *computeBeta.Service
Op *computeBeta.Operation
Project string
Region string
Type ComputeBetaOperationWaitType
Zone string
}

func (w *ComputeBetaOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
return func() (interface{}, string, error) {
var op *computeBeta.Operation
var err error

switch w.Type {
case ComputeBetaOperationWaitGlobal:
op, err = w.Service.GlobalOperations.Get(
w.Project, w.Op.Name).Do()
case ComputeBetaOperationWaitRegion:
op, err = w.Service.RegionOperations.Get(
w.Project, w.Region, w.Op.Name).Do()
case ComputeBetaOperationWaitZone:
op, err = w.Service.ZoneOperations.Get(
w.Project, w.Zone, w.Op.Name).Do()
default:
return nil, "bad-type", fmt.Errorf(
"Invalid wait type: %#v", w.Type)
}

if err != nil {
return nil, "", err
}

log.Printf("[DEBUG] Got %q when asking for operation %q", op.Status, w.Op.Name)

return op, op.Status, nil
}
}

func (w *ComputeBetaOperationWaiter) Conf() *resource.StateChangeConf {
return &resource.StateChangeConf{
Pending: []string{"PENDING", "RUNNING"},
Target: []string{"DONE"},
Refresh: w.RefreshFunc(),
}
}

// ComputeBetaOperationError wraps computeBeta.OperationError and implements the
// error interface so it can be returned.
type ComputeBetaOperationError computeBeta.OperationError

func (e ComputeBetaOperationError) Error() string {
var buf bytes.Buffer

for _, err := range e.Errors {
buf.WriteString(err.Message + "\n")
}

return buf.String()
}

func computeBetaOperationWaitGlobal(config *Config, op *computeBeta.Operation, project string, activity string) error {
return computeBetaOperationWaitGlobalTime(config, op, project, activity, 4)
}

func computeBetaOperationWaitGlobalTime(config *Config, op *computeBeta.Operation, project string, activity string, timeoutMin int) error {
w := &ComputeBetaOperationWaiter{
Service: config.clientComputeBeta,
Op: op,
Project: project,
Type: ComputeBetaOperationWaitGlobal,
}

state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = time.Duration(timeoutMin) * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}

op = opRaw.(*computeBeta.Operation)
if op.Error != nil {
return ComputeBetaOperationError(*op.Error)
}

return nil
}

func computeBetaOperationWaitRegion(config *Config, op *computeBeta.Operation, project string, region, activity string) error {
w := &ComputeBetaOperationWaiter{
Service: config.clientComputeBeta,
Op: op,
Project: project,
Type: ComputeBetaOperationWaitRegion,
Region: region,
}

state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = 4 * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}

op = opRaw.(*computeBeta.Operation)
if op.Error != nil {
return ComputeBetaOperationError(*op.Error)
}

return nil
}

func computeBetaOperationWaitZone(config *Config, op *computeBeta.Operation, project string, zone, activity string) error {
return computeBetaOperationWaitZoneTime(config, op, project, zone, 4, activity)
}

func computeBetaOperationWaitZoneTime(config *Config, op *computeBeta.Operation, project string, zone string, minutes int, activity string) error {
w := &ComputeBetaOperationWaiter{
Service: config.clientComputeBeta,
Op: op,
Project: project,
Zone: zone,
Type: ComputeBetaOperationWaitZone,
}
state := w.Conf()
state.Delay = 10 * time.Second
state.Timeout = time.Duration(minutes) * time.Minute
state.MinTimeout = 2 * time.Second
opRaw, err := state.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for %s: %s", activity, err)
}
op = opRaw.(*computeBeta.Operation)
if op.Error != nil {
// Return the error
return ComputeBetaOperationError(*op.Error)
}
return nil
}
23 changes: 23 additions & 0 deletions google/compute_shared_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package google

import (
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/compute/v1"
)

func computeSharedOperationWaitZone(config *Config, op interface{}, project string, zone, activity string) error {
return computeSharedOperationWaitZoneTime(config, op, project, zone, 4, activity)
}

func computeSharedOperationWaitZoneTime(config *Config, op interface{}, project string, zone string, minutes int, activity string) error {
switch op.(type) {
case *compute.Operation:
return computeOperationWaitZoneTime(config, op.(*compute.Operation), project, zone, minutes, activity)
case *computeBeta.Operation:
return computeBetaOperationWaitZoneTime(config, op.(*computeBeta.Operation), project, zone, minutes, activity)
case nil:
panic("Attempted to wait on an Operation that was nil.")
default:
panic("Attempted to wait on an Operation of unknown type.")
}
}
Loading

0 comments on commit 30b7073

Please sign in to comment.