diff --git a/products/compute/ansible.yaml b/products/compute/ansible.yaml index 215673238b78..c45ac371b180 100644 --- a/products/compute/ansible.yaml +++ b/products/compute/ansible.yaml @@ -26,6 +26,8 @@ manifest: !ruby/object:Provider::Ansible::Manifest datasources: !ruby/object:Overrides::ResourceOverrides Autoscaler: !ruby/object:Overrides::Ansible::ResourceOverride exclude: true + BackendBucketSignedUrlKey: !ruby/object:Overrides::Ansible::ResourceOverride + exclude: true Snapshot: !ruby/object:Overrides::Ansible::ResourceOverride exclude: true ManagedSslCertificate: !ruby/object:Overrides::Ansible::ResourceOverride @@ -223,6 +225,8 @@ overrides: !ruby/object:Overrides::ResourceOverrides # Not yet implemented. Autoscaler: !ruby/object:Overrides::Ansible::ResourceOverride exclude: true + BackendBucketSignedUrlKey: !ruby/object:Overrides::Ansible::ResourceOverride + exclude: true RegionAutoscaler: !ruby/object:Overrides::Ansible::ResourceOverride exclude: true Snapshot: !ruby/object:Overrides::Ansible::ResourceOverride diff --git a/products/compute/api.yaml b/products/compute/api.yaml index 2676ab81e3b2..18039767b882 100644 --- a/products/compute/api.yaml +++ b/products/compute/api.yaml @@ -205,12 +205,45 @@ objects: guides: 'Using a Cloud Storage bucket as a load balancer backend': 'https://cloud.google.com/compute/docs/load-balancing/http/backend-bucket' api: 'https://cloud.google.com/compute/docs/reference/v1/backendBuckets' -<%= indent(compile_file({}, 'templates/global_async.yaml.erb'), 4) %> + async: !ruby/object:Api::Async + operation: !ruby/object:Api::Async::Operation + kind: 'compute#operation' + path: 'name' + base_url: 'projects/{{project}}/global/operations/{{op_id}}' + wait_ms: 1000 + result: !ruby/object:Api::Async::Result + path: 'targetLink' + status: !ruby/object:Api::Async::Status + path: 'status' + complete: 'DONE' + allowed: + - 'PENDING' + - 'RUNNING' + - 'DONE' + error: !ruby/object:Api::Async::Error + path: 'error/errors' + message: 'message' properties: - !ruby/object:Api::Type::String name: 'bucketName' description: 'Cloud Storage bucket name.' required: true + - !ruby/object:Api::Type::NestedObject + name: 'cdnPolicy' + description: 'Cloud CDN configuration for this Backend Bucket.' + properties: + - !ruby/object:Api::Type::Integer + name: 'signedUrlCacheMaxAgeSec' + default_value: 3600 + description: | + Maximum number of seconds the response to a signed URL request will + be considered fresh. Defaults to 1hr (3600s). After this time period, + the response will be revalidated before being served. + When serving responses to signed URL requests, + Cloud CDN will internally behave as though + all responses from this backend had a "Cache-Control: public, + max-age=[TTL]" header, regardless of any existing Cache-Control + header. The actual headers served in responses will not be altered. - !ruby/object:Api::Type::Time name: 'creationTimestamp' description: 'Creation timestamp in RFC3339 text format.' @@ -239,6 +272,73 @@ objects: last character, which cannot be a dash. input: true required: true + - !ruby/object:Api::Resource + name: 'BackendBucketSignedUrlKey' + kind: 'compute#BackendBucketSignedUrlKey' + input: true + base_url: projects/{{project}}/global/backendBuckets/{{backend_bucket}} + create_url: projects/{{project}}/global/backendBuckets/{{backend_bucket}}/addSignedUrlKey + create_verb: :POST + delete_url: projects/{{project}}/global/backendBuckets/{{backend_bucket}}/deleteSignedUrlKey?keyName={{name}} + delete_verb: :POST + self_link: projects/{{project}}/global/backendBuckets/{{backend_bucket}} + identity: + - name + nested_query: !ruby/object:Api::Resource::NestedQuery + keys: + - cdnPolicy + - signedUrlKeyNames + is_list_of_ids: true + description: | + A key for signing Cloud CDN signed URLs for BackendBuckets. + references: !ruby/object:Api::Resource::ReferenceLinks + guides: + 'Using Signed URLs': 'https://cloud.google.com/cdn/docs/using-signed-urls/' + api: 'https://cloud.google.com/compute/docs/reference/rest/v1/backendBuckets' + async: !ruby/object:Api::Async + operation: !ruby/object:Api::Async::Operation + kind: 'compute#operation' + path: 'name' + base_url: 'projects/{{project}}/global/operations/{{op_id}}' + wait_ms: 1000 + result: !ruby/object:Api::Async::Result + path: 'targetLink' + status: !ruby/object:Api::Async::Status + path: 'status' + complete: 'DONE' + allowed: + - 'PENDING' + - 'RUNNING' + - 'DONE' + error: !ruby/object:Api::Async::Error + path: 'error/errors' + message: 'message' + transport: !ruby/object:Api::Resource::Transport + decoder: decode_response + parameters: + - !ruby/object:Api::Type::ResourceRef + name: 'backendBucket' + resource: 'BackendBucket' + imports: 'name' + description: | + The backend bucket this signed URL key belongs. + required: true + input: true + properties: + - !ruby/object:Api::Type::String + name: 'name' + api_name: 'keyName' + description: | + Name of the signed URL key. + required: true + input: true + - !ruby/object:Api::Type::String + name: 'keyValue' + description: | + 128-bit key value used for signing the URL. The key value must be a + valid RFC 4648 Section 5 base64url encoded string. + required: true + input: true - !ruby/object:Api::Resource name: 'BackendService' kind: 'compute#backendService' @@ -250,7 +350,24 @@ objects: description: | Creates a BackendService resource in the specified project using the data included in the request. -<%= indent(compile_file({}, 'templates/global_async.yaml.erb'), 4) %> + async: !ruby/object:Api::Async + operation: !ruby/object:Api::Async::Operation + kind: 'compute#operation' + path: 'name' + base_url: 'projects/{{project}}/global/operations/{{op_id}}' + wait_ms: 1000 + result: !ruby/object:Api::Async::Result + path: 'targetLink' + status: !ruby/object:Api::Async::Status + path: 'status' + complete: 'DONE' + allowed: + - 'PENDING' + - 'RUNNING' + - 'DONE' + error: !ruby/object:Api::Async::Error + path: 'error/errors' + message: 'message' properties: - !ruby/object:Api::Type::Integer name: 'affinityCookieTtlSec' @@ -408,6 +525,20 @@ objects: '&' and '=' will be percent encoded and not treated as delimiters. item_type: Api::Type::String + - !ruby/object:Api::Type::Integer + name: 'signedUrlCacheMaxAgeSec' + default_value: 3600 + description: | + Maximum number of seconds the response to a signed URL request + will be considered fresh, defaults to 1hr (3600s). After this + time period, the response will be revalidated before + being served. + + When serving responses to signed URL requests, Cloud CDN will + internally behave as though all responses from this backend had a + "Cache-Control: public, max-age=[TTL]" header, regardless of any + existing Cache-Control header. The actual headers served in + responses will not be altered. - !ruby/object:Api::Type::NestedObject name: 'connectionDraining' description: 'Settings for connection draining' diff --git a/products/compute/inspec.yaml b/products/compute/inspec.yaml index 5ef80eb89ba6..8be2b3efaed4 100644 --- a/products/compute/inspec.yaml +++ b/products/compute/inspec.yaml @@ -19,6 +19,8 @@ manifest: !ruby/object:Provider::Inspec::Manifest overrides: !ruby/object:Overrides::ResourceOverrides Address: !ruby/object:Overrides::Inspec::ResourceOverride exclude: true + BackendBucketSignedUrlKey: !ruby/object:Overrides::Inspec::ResourceOverride + exclude: true DiskType: !ruby/object:Overrides::Inspec::ResourceOverride exclude: true Firewall: !ruby/object:Overrides::Inspec::ResourceOverride diff --git a/products/compute/terraform.yaml b/products/compute/terraform.yaml index e4fdc13ccc1c..d6e37c5f7c88 100644 --- a/products/compute/terraform.yaml +++ b/products/compute/terraform.yaml @@ -138,6 +138,35 @@ overrides: !ruby/object:Overrides::ResourceOverrides name: !ruby/object:Overrides::Terraform::PropertyOverride validation: !ruby/object:Provider::Terraform::Validation regex: '^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$' + cdnPolicy: !ruby/object:Overrides::Terraform::PropertyOverride + default_from_api: true + BackendBucketSignedUrlKey: !ruby/object:Overrides::Terraform::ResourceOverride + exclude_import: true + mutex: signedUrlKey/{{project}}/backendBuckets/{{backend_bucket}}/ + examples: + - !ruby/object:Provider::Terraform::Examples + name: "backend_bucket_signed_url_key" + primary_resource_id: "backend_key" + vars: + key_name: "test-key" + backend_name: "test-signed-backend-bucket" + bucket_name: "test-storage-bucket" + skip_test: true + properties: + backendBucket: !ruby/object:Overrides::Terraform::PropertyOverride + ignore_read: true + name: !ruby/object:Overrides::Terraform::PropertyOverride + validation: !ruby/object:Provider::Terraform::Validation + regex: '^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$' + keyValue: !ruby/object:Overrides::Terraform::PropertyOverride + sensitive: true + ignore_read: true + docs: !ruby/object:Provider::Terraform::Docs + warning: | + All arguments including the key's value will be stored in the raw + state as plain-text. [Read more about sensitive data in state](/docs/state/sensitive-data.html). + Because the API does not return the sensitive key value, + we cannot confirm or reverse changes to a key outside of Terraform. BackendService: !ruby/object:Overrides::Terraform::ResourceOverride exclude: true Disk: !ruby/object:Overrides::Terraform::ResourceOverride diff --git a/templates/terraform/examples/backend_bucket_signed_url_key.tf.erb b/templates/terraform/examples/backend_bucket_signed_url_key.tf.erb new file mode 100644 index 000000000000..a9341ce2167f --- /dev/null +++ b/templates/terraform/examples/backend_bucket_signed_url_key.tf.erb @@ -0,0 +1,17 @@ +resource "google_compute_backend_bucket_signed_url_key" "backend_key" { + name = "<%= ctx[:vars]['key_name'] %>" + key_value = "pPsVemX8GM46QVeezid6Rw==" + backend_bucket = "${google_compute_backend_bucket.test_backend.name}" +} + +resource "google_compute_backend_bucket" "test_backend" { + name = "<%= ctx[:vars]['backend_name'] %>" + description = "Contains beautiful images" + bucket_name = "${google_storage_bucket.bucket.name}" + enable_cdn = true +} + +resource "google_storage_bucket" "bucket" { + name = "<%= ctx[:vars]['bucket_name'] %>" + location = "EU" +} diff --git a/third_party/terraform/tests/resource_compute_backend_bucket_signed_url_key_test.go b/third_party/terraform/tests/resource_compute_backend_bucket_signed_url_key_test.go new file mode 100644 index 000000000000..23aaeaa903a6 --- /dev/null +++ b/third_party/terraform/tests/resource_compute_backend_bucket_signed_url_key_test.go @@ -0,0 +1,119 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "strings" +) + +func TestAccComputeBackendBucketSignedUrlKey_basic(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(10), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeBackendBucketSignedUrlKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccComputeBackendBucketSignedUrlKey_basic(context), + Check: testAccCheckComputeBackendBucketSignedUrlKeyCreated, + }, + }, + }) +} + +func testAccComputeBackendBucketSignedUrlKey_basic(context map[string]interface{}) string { + return Nprintf(` +resource "google_compute_backend_bucket_signed_url_key" "backend_key" { + name = "test-key-%{random_suffix}" + key_value = "iAmAFakeKeyRandomBytes==" + backend_bucket = "${google_compute_backend_bucket.test_backend.name}" +} + +resource "google_compute_backend_bucket" "test_backend" { + name = "test-signed-backend-bucket-%{random_suffix}" + description = "Contains beautiful images" + bucket_name = "${google_storage_bucket.bucket.name}" + enable_cdn = true +} + +resource "google_storage_bucket" "bucket" { + name = "test-storage-bucket-%{random_suffix}" + location = "EU" +} +`, context) +} + +func testAccCheckComputeBackendBucketSignedUrlKeyDestroy(s *terraform.State) error { + exists, err := checkComputeBackendBucketSignedUrlKeyExists(s) + if err != nil && !isGoogleApiErrorWithCode(err, 404) { + return err + } + if exists { + return fmt.Errorf("ComputeBackendBucketSignedUrlKey still exists") + } + return nil +} + +func testAccCheckComputeBackendBucketSignedUrlKeyCreated(s *terraform.State) error { + exists, err := checkComputeBackendBucketSignedUrlKeyExists(s) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("expected ComputeBackendBucketSignedUrlKey to have been created") + } + return nil +} + +func checkComputeBackendBucketSignedUrlKeyExists(s *terraform.State) (bool, error) { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_backend_bucket_signed_url_key" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := testAccProvider.Meta().(*Config) + keyName := rs.Primary.ID + + url, err := replaceVarsForTest(rs, "https://www.googleapis.com/compute/v1/projects/{{project}}/global/backendBuckets/{{backend_bucket}}") + if err != nil { + return false, err + } + + res, err := sendRequest(config, "GET", url, nil) + if err == nil { + policyRaw, ok := res["cdnPolicy"] + if !ok { + return false, nil + } + + policy := policyRaw.(map[string]interface{}) + keyNames, ok := policy["signedUrlKeyNames"] + if !ok { + return false, nil + } + + // Because the sensitive key value is not returned, all we can do is verify a + // key with this name exists and assume the key value hasn't been changed. + for _, k := range keyNames.([]interface{}) { + if k.(string) == keyName { + // Just return empty map to indicate key was found + return true, nil + } + } + } + } + + return false, nil +} diff --git a/third_party/terraform/tests/resource_compute_backend_bucket_test.go b/third_party/terraform/tests/resource_compute_backend_bucket_test.go index 7aa39d159010..a32ad17d778d 100644 --- a/third_party/terraform/tests/resource_compute_backend_bucket_test.go +++ b/third_party/terraform/tests/resource_compute_backend_bucket_test.go @@ -6,8 +6,6 @@ import ( "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" - "google.golang.org/api/compute/v1" ) func TestAccComputeBackendBucket_basicModified(t *testing.T) { @@ -16,7 +14,6 @@ func TestAccComputeBackendBucket_basicModified(t *testing.T) { backendName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) storageName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) secondStorageName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) - var svc compute.BackendBucket resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -25,62 +22,30 @@ func TestAccComputeBackendBucket_basicModified(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccComputeBackendBucket_basic(backendName, storageName), - Check: resource.ComposeTestCheckFunc( - testAccCheckComputeBackendBucketExists( - "google_compute_backend_bucket.foobar", &svc), - ), + }, + { + ResourceName: "google_compute_backend_bucket.foobar", + ImportState: true, + ImportStateVerify: true, }, { Config: testAccComputeBackendBucket_basicModified( backendName, storageName, secondStorageName), - Check: resource.ComposeTestCheckFunc( - testAccCheckComputeBackendBucketExists( - "google_compute_backend_bucket.foobar", &svc), - ), + }, + { + ResourceName: "google_compute_backend_bucket.foobar", + ImportState: true, + ImportStateVerify: true, }, }, }) - - if svc.BucketName != secondStorageName { - t.Errorf("Expected BucketName to be %q, got %q", secondStorageName, svc.BucketName) - } -} - -func testAccCheckComputeBackendBucketExists(n string, svc *compute.BackendBucket) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - config := testAccProvider.Meta().(*Config) - - found, err := config.clientCompute.BackendBuckets.Get( - config.Project, rs.Primary.ID).Do() - if err != nil { - return err - } - - if found.Name != rs.Primary.ID { - return fmt.Errorf("Backend bucket %s not found", rs.Primary.ID) - } - - *svc = *found - - return nil - } } -func TestAccComputeBackendBucket_withCdnEnabled(t *testing.T) { +func TestAccComputeBackendBucket_withCdnPolicy(t *testing.T) { t.Parallel() backendName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) storageName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) - var svc compute.BackendBucket resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -88,19 +53,15 @@ func TestAccComputeBackendBucket_withCdnEnabled(t *testing.T) { CheckDestroy: testAccCheckComputeBackendBucketDestroy, Steps: []resource.TestStep{ { - Config: testAccComputeBackendBucket_withCdnEnabled( - backendName, storageName), - Check: resource.ComposeTestCheckFunc( - testAccCheckComputeBackendBucketExists( - "google_compute_backend_bucket.foobar", &svc), - ), + Config: testAccComputeBackendBucket_withCdnPolicy(backendName, storageName), + }, + { + ResourceName: "google_compute_backend_bucket.foobar", + ImportState: true, + ImportStateVerify: true, }, }, }) - - if svc.EnableCdn != true { - t.Errorf("Expected EnableCdn == true, got %t", svc.EnableCdn) - } } func testAccComputeBackendBucket_basic(backendName, storageName string) string { @@ -136,12 +97,15 @@ resource "google_storage_bucket" "bucket_two" { `, backendName, bucketOne, bucketTwo) } -func testAccComputeBackendBucket_withCdnEnabled(backendName, storageName string) string { +func testAccComputeBackendBucket_withCdnPolicy(backendName, storageName string) string { return fmt.Sprintf(` resource "google_compute_backend_bucket" "foobar" { name = "%s" bucket_name = "${google_storage_bucket.bucket.name}" enable_cdn = true + cdn_policy { + signed_url_cache_max_age_sec = 1000 + } } resource "google_storage_bucket" "bucket" {