diff --git a/google/resource_compute_backend_bucket.go b/google/resource_compute_backend_bucket.go index 804da80a..0a501f62 100644 --- a/google/resource_compute_backend_bucket.go +++ b/google/resource_compute_backend_bucket.go @@ -1,3 +1,17 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated and manual changes will be +// clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + package google import ( @@ -5,7 +19,7 @@ import ( "log" "github.com/hashicorp/terraform/helper/schema" - "google.golang.org/api/compute/v1" + compute "google.golang.org/api/compute/v1" ) func resourceComputeBackendBucket() *schema.Resource { @@ -16,40 +30,38 @@ func resourceComputeBackendBucket() *schema.Resource { Delete: resourceComputeBackendBucketDelete, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + State: resourceComputeBackendBucketImport, }, Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validateGCPName, - }, - - "bucket_name": &schema.Schema{ + "bucket_name": { Type: schema.TypeString, Required: true, }, - - "description": &schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateRegexp(`^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$`), + }, + "description": { Type: schema.TypeString, Optional: true, }, - - "enable_cdn": &schema.Schema{ + "enable_cdn": { Type: schema.TypeBool, Optional: true, - Default: false, }, - + "creation_timestamp": { + Type: schema.TypeString, + Computed: true, + }, "project": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, - "self_link": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -61,38 +73,43 @@ func resourceComputeBackendBucket() *schema.Resource { func resourceComputeBackendBucketCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - bucket := compute.BackendBucket{ - Name: d.Get("name").(string), - BucketName: d.Get("bucket_name").(string), - } - - if v, ok := d.GetOk("description"); ok { - bucket.Description = v.(string) - } - - if v, ok := d.GetOk("enable_cdn"); ok { - bucket.EnableCdn = v.(bool) - } - project, err := getProject(d, config) if err != nil { return err } - log.Printf("[DEBUG] Creating new Backend Bucket: %#v", bucket) - op, err := config.clientCompute.BackendBuckets.Insert( - project, &bucket).Do() - if err != nil { - return fmt.Errorf("Error creating backend bucket: %s", err) + obj := map[string]interface{}{ + "bucketName": expandComputeBackendBucketBucketName(d.Get("bucket_name")), + "description": expandComputeBackendBucketDescription(d.Get("description")), + "enableCdn": expandComputeBackendBucketEnableCdn(d.Get("enable_cdn")), + "name": expandComputeBackendBucketName(d.Get("name")), } - log.Printf("[DEBUG] Waiting for new backend bucket, operation: %#v", op) + url, err := replaceVars(d, config, "https://www.googleapis.com/compute/v1/projects/{{project}}/global/backendBuckets") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new BackendBucket: %#v", obj) + res, err := Post(config, url, obj) + if err != nil { + return fmt.Errorf("Error creating BackendBucket: %s", err) + } // Store the ID now - d.SetId(bucket.Name) + id, err := replaceVars(d, config, "{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) - // Wait for the operation to complete - waitErr := computeOperationWait(config.clientCompute, op, project, "Creating Backend Bucket") + op := &compute.Operation{} + err = Convert(res, op) + if err != nil { + return err + } + + waitErr := computeOperationWait(config.clientCompute, op, project, "Creating BackendBucket") if waitErr != nil { // The resource didn't actually create d.SetId("") @@ -110,18 +127,23 @@ func resourceComputeBackendBucketRead(d *schema.ResourceData, meta interface{}) return err } - bucket, err := config.clientCompute.BackendBuckets.Get( - project, d.Id()).Do() + url, err := replaceVars(d, config, "https://www.googleapis.com/compute/v1/projects/{{project}}/global/backendBuckets/{{name}}") if err != nil { - return handleNotFoundError(err, d, fmt.Sprintf("Backend Bucket %q", d.Get("name").(string))) + return err } - d.Set("name", bucket.Name) - d.Set("bucket_name", bucket.BucketName) - d.Set("description", bucket.Description) - d.Set("enable_cdn", bucket.EnableCdn) + res, err := Get(config, url) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("ComputeBackendBucket %q", d.Id())) + } + + d.Set("bucket_name", flattenComputeBackendBucketBucketName(res["bucketName"])) + d.Set("creation_timestamp", flattenComputeBackendBucketCreationTimestamp(res["creationTimestamp"])) + d.Set("description", flattenComputeBackendBucketDescription(res["description"])) + d.Set("enable_cdn", flattenComputeBackendBucketEnableCdn(res["enableCdn"])) + d.Set("name", flattenComputeBackendBucketName(res["name"])) + d.Set("self_link", res["selfLink"]) d.Set("project", project) - d.Set("self_link", bucket.SelfLink) return nil } @@ -134,30 +156,31 @@ func resourceComputeBackendBucketUpdate(d *schema.ResourceData, meta interface{} return err } - bucket := compute.BackendBucket{ - Name: d.Get("name").(string), - BucketName: d.Get("bucket_name").(string), + obj := map[string]interface{}{ + "bucketName": expandComputeBackendBucketBucketName(d.Get("bucket_name")), + "description": expandComputeBackendBucketDescription(d.Get("description")), + "enableCdn": expandComputeBackendBucketEnableCdn(d.Get("enable_cdn")), + "name": expandComputeBackendBucketName(d.Get("name")), } - // Optional things - if v, ok := d.GetOk("description"); ok { - bucket.Description = v.(string) - } - - if v, ok := d.GetOk("enable_cdn"); ok { - bucket.EnableCdn = v.(bool) - } - - log.Printf("[DEBUG] Updating existing Backend Bucket %q: %#v", d.Id(), bucket) - op, err := config.clientCompute.BackendBuckets.Update( - project, d.Id(), &bucket).Do() + url, err := replaceVars(d, config, "https://www.googleapis.com/compute/v1/projects/{{project}}/global/backendBuckets/{{name}}") if err != nil { - return fmt.Errorf("Error updating backend bucket: %s", err) + return err } - d.SetId(bucket.Name) + log.Printf("[DEBUG] Updating BackendBucket %q: %#v", d.Id(), obj) + res, err := Put(config, url, obj) + if err != nil { + return fmt.Errorf("Error updating BackendBucket %q: %s", d.Id(), err) + } - err = computeOperationWait(config.clientCompute, op, project, "Updating Backend Bucket") + op := &compute.Operation{} + err = Convert(res, op) + if err != nil { + return err + } + + err = computeOperationWait(config.clientCompute, op, project, "Updating BackendBucket") if err != nil { return err } @@ -173,18 +196,68 @@ func resourceComputeBackendBucketDelete(d *schema.ResourceData, meta interface{} return err } - log.Printf("[DEBUG] Deleting backend bucket %s", d.Id()) - op, err := config.clientCompute.BackendBuckets.Delete( - project, d.Id()).Do() - if err != nil { - return fmt.Errorf("Error deleting backend bucket: %s", err) - } - - err = computeOperationWait(config.clientCompute, op, project, "Deleting Backend Bucket") + url, err := replaceVars(d, config, "https://www.googleapis.com/compute/v1/projects/{{project}}/global/backendBuckets/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting BackendBucket %q", d.Id()) + res, err := Delete(config, url) + if err != nil { + return fmt.Errorf("Error deleting BackendBucket %q: %s", d.Id(), err) + } + + op := &compute.Operation{} + err = Convert(res, op) + if err != nil { + return err + } + + err = computeOperationWait(config.clientCompute, op, project, "Deleting BackendBucket") if err != nil { return err } - d.SetId("") return nil } + +func resourceComputeBackendBucketImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.Set("name", d.Id()) + return []*schema.ResourceData{d}, nil +} + +func flattenComputeBackendBucketBucketName(v interface{}) interface{} { + return v +} + +func flattenComputeBackendBucketCreationTimestamp(v interface{}) interface{} { + return v +} + +func flattenComputeBackendBucketDescription(v interface{}) interface{} { + return v +} + +func flattenComputeBackendBucketEnableCdn(v interface{}) interface{} { + return v +} + +func flattenComputeBackendBucketName(v interface{}) interface{} { + return v +} + +func expandComputeBackendBucketBucketName(v interface{}) interface{} { + return v +} + +func expandComputeBackendBucketDescription(v interface{}) interface{} { + return v +} + +func expandComputeBackendBucketEnableCdn(v interface{}) interface{} { + return v +} + +func expandComputeBackendBucketName(v interface{}) interface{} { + return v +} diff --git a/google/transport.go b/google/transport.go new file mode 100644 index 00000000..94a6500c --- /dev/null +++ b/google/transport.go @@ -0,0 +1,176 @@ +package google + +import ( + "bytes" + "encoding/json" + "net/http" + "regexp" + "strings" + + "reflect" + + "google.golang.org/api/googleapi" +) + +type serializableBody struct { + body map[string]interface{} + + // ForceSendFields is a list of field names (e.g. "UtilizationTarget") + // to unconditionally include in API requests. By default, fields with + // empty values are omitted from API requests. However, any non-pointer, + // non-interface field appearing in ForceSendFields will be sent to the + // server regardless of whether the field is empty or not. This may be + // used to include empty fields in Patch requests. + ForceSendFields []string + + // NullFields is a list of field names (e.g. "UtilizationTarget") to + // include in API requests with the JSON null value. By default, fields + // with empty values are omitted from API requests. However, any field + // with an empty value appearing in NullFields will be sent to the + // server as null. It is an error if a field in this list has a + // non-empty value. This may be used to include null fields in Patch + // requests. + NullFields []string +} + +// MarshalJSON returns a JSON encoding of schema containing only selected fields. +// A field is selected if any of the following is true: +// * it has a non-empty value +// * its field name is present in forceSendFields and it is not a nil pointer or nil interface +// * its field name is present in nullFields. +func (b *serializableBody) MarshalJSON() ([]byte, error) { + // By default, all fields in a map are added to the json output + // This changes that to remove the entry with an empty value. + // This mimics the "omitempty" behavior. + + // The "omitempty" option specifies that the field should be omitted + // from the encoding if the field has an empty value, defined as + // false, 0, a nil pointer, a nil interface value, and any empty array, + // slice, map, or string. + + // TODO: Add support for ForceSendFields and NullFields. + for k, v := range b.body { + if isEmptyValue(reflect.ValueOf(v)) { + delete(b.body, k) + } + } + + return json.Marshal(b.body) +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func Post(config *Config, url string, body map[string]interface{}) (map[string]interface{}, error) { + return sendRequest(config, "POST", url, body) +} + +func Get(config *Config, url string) (map[string]interface{}, error) { + return sendRequest(config, "GET", url, nil) +} + +func Put(config *Config, url string, body map[string]interface{}) (map[string]interface{}, error) { + return sendRequest(config, "PUT", url, body) +} + +func Delete(config *Config, url string) (map[string]interface{}, error) { + return sendRequest(config, "DELETE", url, nil) +} + +func sendRequest(config *Config, method, url string, body map[string]interface{}) (map[string]interface{}, error) { + reqHeaders := make(http.Header) + reqHeaders.Set("User-Agent", config.userAgent) + reqHeaders.Set("Content-Type", "application/json") + + var buf bytes.Buffer + if body != nil { + err := json.NewEncoder(&buf).Encode(&serializableBody{ + body: body}) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, url+"?alt=json", &buf) + if err != nil { + return nil, err + } + req.Header = reqHeaders + res, err := config.client.Do(req) + if err != nil { + return nil, err + } + defer googleapi.CloseBody(res) + if err := googleapi.CheckResponse(res); err != nil { + return nil, err + } + + result := make(map[string]interface{}) + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, err + } + + return result, nil +} + +func replaceVars(d TerraformResourceData, config *Config, linkTmpl string) (string, error) { + re := regexp.MustCompile("{{([[:word:]]+)}}") + var project, region, zone string + var err error + + if strings.Contains(linkTmpl, "{{project}}") { + project, err = getProject(d, config) + if err != nil { + return "", err + } + } + + if strings.Contains(linkTmpl, "{{region}}") { + region, err = getRegion(d, config) + if err != nil { + return "", err + } + } + + if strings.Contains(linkTmpl, "{{zone}}") { + zone, err = getZone(d, config) + if err != nil { + return "", err + } + } + + replaceFunc := func(s string) string { + m := re.FindStringSubmatch(s)[1] + if m == "project" { + return project + } + if m == "region" { + return region + } + if m == "zone" { + return zone + } + v, ok := d.GetOk(m) + if ok { + return v.(string) + } + return "" + } + + return re.ReplaceAllStringFunc(linkTmpl, replaceFunc), nil +} diff --git a/google/transport_test.go b/google/transport_test.go new file mode 100644 index 00000000..727e7bc2 --- /dev/null +++ b/google/transport_test.go @@ -0,0 +1,96 @@ +package google + +import ( + "testing" +) + +func TestReplaceVars(t *testing.T) { + cases := map[string]struct { + Template string + SchemaValues map[string]interface{} + Config *Config + Expected string + ExpectedError bool + }{ + "unspecified project fails": { + Template: "projects/{{project}}/global/images", + ExpectedError: true, + }, + "unspecified region fails": { + Template: "projects/{{project}}/regions/{{region}}/subnetworks", + Config: &Config{ + Project: "default-project", + }, + ExpectedError: true, + }, + "unspecified zone fails": { + Template: "projects/{{project}}/zones/{{zone}}/instances", + Config: &Config{ + Project: "default-project", + }, + ExpectedError: true, + }, + "regional with default values": { + Template: "projects/{{project}}/regions/{{region}}/subnetworks", + Config: &Config{ + Project: "default-project", + Region: "default-region", + }, + Expected: "projects/default-project/regions/default-region/subnetworks", + }, + "zonal with default values": { + Template: "projects/{{project}}/zones/{{zone}}/instances", + Config: &Config{ + Project: "default-project", + Zone: "default-zone", + }, + Expected: "projects/default-project/zones/default-zone/instances", + }, + "regional schema values": { + Template: "projects/{{project}}/regions/{{region}}/subnetworks/{{name}}", + SchemaValues: map[string]interface{}{ + "project": "project1", + "region": "region1", + "name": "subnetwork1", + }, + Expected: "projects/project1/regions/region1/subnetworks/subnetwork1", + }, + "zonal schema values": { + Template: "projects/{{project}}/zones/{{zone}}/instances/{{name}}", + SchemaValues: map[string]interface{}{ + "project": "project1", + "zone": "zone1", + "name": "instance1", + }, + Expected: "projects/project1/zones/zone1/instances/instance1", + }, + } + + for tn, tc := range cases { + d := &ResourceDataMock{ + FieldsInSchema: tc.SchemaValues, + } + + config := tc.Config + if config == nil { + config = &Config{} + } + + v, err := replaceVars(d, config, tc.Template) + + if err != nil { + if !tc.ExpectedError { + t.Errorf("bad: %s; unexpected error %s", tn, err) + } + continue + } + + if tc.ExpectedError { + t.Errorf("bad: %s; expected error", tn) + } + + if v != tc.Expected { + t.Errorf("bad: %s; expected %q, got %q", tn, tc.Expected, v) + } + } +} diff --git a/website/docs/r/compute_backend_bucket.html.markdown b/website/docs/r/compute_backend_bucket.html.markdown index 6866bb5e..82b2560e 100644 --- a/website/docs/r/compute_backend_bucket.html.markdown +++ b/website/docs/r/compute_backend_bucket.html.markdown @@ -52,6 +52,8 @@ The following arguments are supported: In addition to the arguments listed above, the following computed attributes are exported: +* `creation_timestamp` - Creation timestamp in RFC3339 text format. + * `self_link` - The URI of the created resource. ## Import