diff --git a/google/config_builder.go b/google/config_builder.go new file mode 100644 index 00000000000..c9e08ad0b77 --- /dev/null +++ b/google/config_builder.go @@ -0,0 +1,125 @@ +package google + +import ( + "bytes" + "fmt" +) + +// ConfigBuilder is a helper class for generating Terraform config strings for use in tests. +type ConfigBuilder struct { + // ResourceName is the name of the resource (e.g. the 'foo' in 'resource "google_compute_instance" "foo" {'). + ResourceName string + // ResourceType is the type of the resource (e.g. the 'google_compute_instance' in 'resource + // "google_compute_instance" "foo" {'). + ResourceType string + // Attributes contains a mapping between all key/value pairs. + Attributes map[string]interface{} +} + +// NewResourceConfigBuilder creates a ConfigBuilder for a resource with the provided type and name. +func NewResourceConfigBuilder(typ, name string) *ConfigBuilder { + return &ConfigBuilder{ResourceName: name, ResourceType: typ, Attributes: map[string]interface{}{}} +} + +// NewNestedConfig is used for nesting maps (e.g. if you wanted to add a set of key/values for labels, you'd do +// something like: +// +// x := NewResourceConfigBuilder("google_container_cluster", "cluster-" + acctest.RandString(10)). +// WithAttribute("labels", NewNestedConfig(). +// WithAttribute("my_label", "my_value")) +func NewNestedConfig() *ConfigBuilder { + return &ConfigBuilder{Attributes: map[string]interface{}{}} +} + +// WithResourceName sets the Terraform resource name. +func (rb *ConfigBuilder) WithResourceName(name string) *ConfigBuilder { + rb.ResourceName = name + return rb +} + +// WithResourceType sets the Terraform resource type. +func (rb *ConfigBuilder) WithResourceType(typ string) *ConfigBuilder { + rb.ResourceType = typ + return rb +} + +// WithAttribute sets an attribute on the resource. Anything that implements the Stringer interface or is a primitive +// can be used here. See NewNestedConfig() as well for an example on how to embed an additional map structure. +func (rb *ConfigBuilder) WithAttribute(key string, obj interface{}) *ConfigBuilder { + rb.Attributes[key] = obj + return rb +} + +// Name returns the "name" attribute (commonly used in GCP resources). +func (rb ConfigBuilder) Name() string { + return rb.Attributes["name"].(string) +} + +// WithName sets the "name" attribute (commonly used in GCP resources). +func (rb *ConfigBuilder) WithName(name string) *ConfigBuilder { + rb.Attributes["name"] = name + return rb +} + +// Zone returns the "zone" attribute (commonly used in GCP resources). +func (rb ConfigBuilder) Zone() string { + return rb.Attributes["zone"].(string) +} + +// WithZone sets the "zone" attribute (commonly used in GCP resources). +func (rb *ConfigBuilder) WithZone(zone string) *ConfigBuilder { + rb.Attributes["zone"] = zone + return rb +} + +// String returns a pretty-printed string of the config. +func (rb ConfigBuilder) String() string { + return rb.StringWithIndent(0, 4, false) +} + +type StringWithIndenter interface { + // StringWithIndent is like String, but allows for control of multiline resources. 'indent' represents how much to + // indent every line. 'indentLen' controls how much indenting to add when adding additional indentation. 'embedded' + // represents whether or not the produced string is embedded in a larger structure, in which case the leading + // indentation on the first line is suppressed. + StringWithIndent(indent, indentLen int, embedded bool) string +} + +func (rb ConfigBuilder) StringWithIndent(indent, indentLen int, embedded bool) string { + var buf bytes.Buffer + + if !embedded { + buf.WriteString(spacesOfLength(indent)) + } + if rb.ResourceName != "" && rb.ResourceType != "" { + buf.WriteString(fmt.Sprintf("resource \"%s\" \"%s\" ", rb.ResourceType, rb.ResourceName)) + } + buf.WriteString("{\n") + for k, v := range rb.Attributes { + buf.WriteString(spacesOfLength(indent + indentLen)) + buf.WriteString(fmt.Sprintf("%s ", k)) + switch v.(type) { + case int: + buf.WriteString(fmt.Sprintf("= %d", v)) + case string: + buf.WriteString(fmt.Sprintf("= \"%s\"", v)) + case StringWithIndenter: + buf.WriteString("" + v.(StringWithIndenter).StringWithIndent(indent+indentLen, indentLen, true)) + case fmt.Stringer: + buf.WriteString("= " + v.(fmt.Stringer).String()) + } + buf.WriteString("\n") + } + + buf.WriteString(spacesOfLength(indent) + "}\n") + return buf.String() +} + +// spacesOfLength is a helper function for generating a string consisting of just spaces. +func spacesOfLength(len int) string { + sp := make([]byte, len) + for idx := range sp { + sp[idx] = byte(' ') + } + return string(sp) +} diff --git a/google/resource_container_node_pool_test.go b/google/resource_container_node_pool_test.go index a6b0da80921..16913f3e038 100644 --- a/google/resource_container_node_pool_test.go +++ b/google/resource_container_node_pool_test.go @@ -2,24 +2,35 @@ package google import ( "fmt" - "strconv" "testing" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/container/v1" ) func TestAccContainerNodePool_basic(t *testing.T) { + name := "tf-nodepool-test-" + acctest.RandString(10) + zone := "us-central1-a" + clusterConfig := SomeGoogleContainerCluster() + nodepoolConfig := SomeGoogleContainerNodePool(clusterConfig). + WithAttribute("name", name). + WithAttribute("zone", zone). + WithAttribute("initial_node_count", 2) + + var nodePool container.NodePool + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckContainerNodePoolDestroy, Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccContainerNodePool_basic, + { + Config: clusterConfig.String() + nodepoolConfig.String(), Check: resource.ComposeTestCheckFunc( - testAccCheckContainerNodePoolMatches("google_container_node_pool.np"), + testAccCheckContainerNodePoolExists(zone, clusterConfig.Name(), name, &nodePool), + testAccCheckContainerNodePoolHasInitialNodeCount(&nodePool, 2), ), }, }, @@ -45,57 +56,47 @@ func testAccCheckContainerNodePoolDestroy(s *terraform.State) error { return nil } -func testAccCheckContainerNodePoolMatches(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - config := testAccProvider.Meta().(*Config) +func SomeGoogleContainerCluster() *ConfigBuilder { + return NewResourceConfigBuilder("google_container_cluster", "cluster-"+acctest.RandString(10)). + WithAttribute("name", "tf-cluster-nodepool-test-"+acctest.RandString(10)). + WithAttribute("zone", "us-central1-a"). + WithAttribute("initial_node_count", 3). + WithAttribute("master_auth", NewNestedConfig(). + WithAttribute("username", "mr.yoda"). + WithAttribute("password", "adoy.rm")) +} - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } +func SomeGoogleContainerNodePool(cluster *ConfigBuilder) *ConfigBuilder { + return NewResourceConfigBuilder("google_container_node_pool", "nodepool-"+acctest.RandString(10)). + WithAttribute("name", "tf-nodepool-test-"+acctest.RandString(10)). + WithAttribute("zone", "us-central1-a"). + WithAttribute("cluster", fmt.Sprintf("${google_container_cluster.%s.name}", cluster.ResourceName)). + WithAttribute("initial_node_count", 2) +} - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } +func testAccCheckContainerNodePoolExists(zone, clusterName, nodePoolName string, nodePool *container.NodePool) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) - attributes := rs.Primary.Attributes - found, err := config.clientContainer.Projects.Zones.Clusters.NodePools.Get( - config.Project, attributes["zone"], attributes["cluster"], attributes["name"]).Do() + found, err := config.clientContainer.Projects.Zones.Clusters.NodePools.Get(config.Project, zone, clusterName, nodePoolName).Do() if err != nil { return err } - if found.Name != attributes["name"] { - return fmt.Errorf("NodePool not found") + if found == nil { + return fmt.Errorf("Unable to find resource") } - inc, err := strconv.Atoi(attributes["initial_node_count"]) - if err != nil { - return err - } - if found.InitialNodeCount != int64(inc) { - return fmt.Errorf("Mismatched initialNodeCount. TF State: %s. GCP State: %d", - attributes["initial_node_count"], found.InitialNodeCount) - } + *nodePool = *found return nil } } -var testAccContainerNodePool_basic = fmt.Sprintf(` -resource "google_container_cluster" "cluster" { - name = "tf-cluster-nodepool-test-%s" - zone = "us-central1-a" - initial_node_count = 3 - - master_auth { - username = "mr.yoda" - password = "adoy.rm" +func testAccCheckContainerNodePoolHasInitialNodeCount(nodePool *container.NodePool, count int64) resource.TestCheckFunc { + return func(s *terraform.State) error { + if nodePool.InitialNodeCount != count { + return fmt.Errorf("Expected initial_node_count %d but found %d", count, nodePool.InitialNodeCount) + } + return nil } } - -resource "google_container_node_pool" "np" { - name = "tf-nodepool-test-%s" - zone = "us-central1-a" - cluster = "${google_container_cluster.cluster.name}" - initial_node_count = 2 -}`, acctest.RandString(10), acctest.RandString(10))