diff --git a/google/resource_source_repo_repository.go b/google/resource_source_repo_repository.go index fc12212934c..cfc4f22e0cd 100644 --- a/google/resource_source_repo_repository.go +++ b/google/resource_source_repo_repository.go @@ -23,12 +23,14 @@ import ( "time" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" ) func resourceSourceRepoRepository() *schema.Resource { return &schema.Resource{ Create: resourceSourceRepoRepositoryCreate, Read: resourceSourceRepoRepositoryRead, + Update: resourceSourceRepoRepositoryUpdate, Delete: resourceSourceRepoRepositoryDelete, Importer: &schema.ResourceImporter{ @@ -37,6 +39,7 @@ func resourceSourceRepoRepository() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(4 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Minute), Delete: schema.DefaultTimeout(4 * time.Minute), }, @@ -48,6 +51,37 @@ func resourceSourceRepoRepository() *schema.Resource { Description: `Resource name of the repository, of the form '{{repo}}'. The repo name may contain slashes. eg, 'name/with/slash'`, }, + "pubsub_configs": { + Type: schema.TypeSet, + Optional: true, + Description: `How this repository publishes a change in the repository through Cloud Pub/Sub. +Keyed by the topic names.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "topic": { + Type: schema.TypeString, + Required: true, + }, + "message_format": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"PROTOBUF", "JSON"}, false), + Description: `The format of the Cloud Pub/Sub messages. +- PROTOBUF: The message payload is a serialized protocol buffer of SourceRepoEvent. +- JSON: The message payload is a JSON string of SourceRepoEvent.`, + }, + "service_account_email": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Email address of the service account used for publishing Cloud Pub/Sub messages. +This service account needs to be in the same project as the PubsubConfig. When added, +the caller needs to have iam.serviceAccounts.actAs permission on this service account. +If unspecified, it defaults to the compute engine default service account.`, + }, + }, + }, + }, "size": { Type: schema.TypeInt, Computed: true, @@ -78,6 +112,12 @@ func resourceSourceRepoRepositoryCreate(d *schema.ResourceData, meta interface{} } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { obj["name"] = nameProp } + pubsubConfigsProp, err := expandSourceRepoRepositoryPubsubConfigs(d.Get("pubsub_configs"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("pubsub_configs"); !isEmptyValue(reflect.ValueOf(pubsubConfigsProp)) && (ok || !reflect.DeepEqual(v, pubsubConfigsProp)) { + obj["pubsubConfigs"] = pubsubConfigsProp + } url, err := replaceVars(d, config, "{{SourceRepoBasePath}}projects/{{project}}/repos") if err != nil { @@ -103,6 +143,12 @@ func resourceSourceRepoRepositoryCreate(d *schema.ResourceData, meta interface{} log.Printf("[DEBUG] Finished creating Repository %q: %#v", d.Id(), res) + if v, ok := d.GetOkExists("pubsub_configs"); !isEmptyValue(reflect.ValueOf(pubsubConfigsProp)) && (ok || !reflect.DeepEqual(v, pubsubConfigsProp)) { + log.Printf("[DEBUG] Calling update after create to patch in pubsub_configs") + // pubsub_configs cannot be added on create + return resourceSourceRepoRepositoryUpdate(d, meta) + } + return resourceSourceRepoRepositoryRead(d, meta) } @@ -136,10 +182,60 @@ func resourceSourceRepoRepositoryRead(d *schema.ResourceData, meta interface{}) if err := d.Set("size", flattenSourceRepoRepositorySize(res["size"], d)); err != nil { return fmt.Errorf("Error reading Repository: %s", err) } + if err := d.Set("pubsub_configs", flattenSourceRepoRepositoryPubsubConfigs(res["pubsubConfigs"], d)); err != nil { + return fmt.Errorf("Error reading Repository: %s", err) + } return nil } +func resourceSourceRepoRepositoryUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + pubsubConfigsProp, err := expandSourceRepoRepositoryPubsubConfigs(d.Get("pubsub_configs"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("pubsub_configs"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, pubsubConfigsProp)) { + obj["pubsubConfigs"] = pubsubConfigsProp + } + + obj, err = resourceSourceRepoRepositoryUpdateEncoder(d, meta, obj) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{SourceRepoBasePath}}projects/{{project}}/repos/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Repository %q: %#v", d.Id(), obj) + updateMask := []string{} + + if d.HasChange("pubsub_configs") { + updateMask = append(updateMask, "pubsubConfigs") + } + // updateMask is a URL parameter but not present in the schema, so replaceVars + // won't set it + url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + _, err = sendRequestWithTimeout(config, "PATCH", project, url, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Repository %q: %s", d.Id(), err) + } + + return resourceSourceRepoRepositoryRead(d, meta) +} + func resourceSourceRepoRepositoryDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) @@ -209,6 +305,80 @@ func flattenSourceRepoRepositorySize(v interface{}, d *schema.ResourceData) inte return v } +func flattenSourceRepoRepositoryPubsubConfigs(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return v + } + l := v.(map[string]interface{}) + transformed := make([]interface{}, 0, len(l)) + for k, raw := range l { + original := raw.(map[string]interface{}) + transformed = append(transformed, map[string]interface{}{ + "topic": k, + "message_format": flattenSourceRepoRepositoryPubsubConfigsMessageFormat(original["messageFormat"], d), + "service_account_email": flattenSourceRepoRepositoryPubsubConfigsServiceAccountEmail(original["serviceAccountEmail"], d), + }) + } + return transformed +} +func flattenSourceRepoRepositoryPubsubConfigsMessageFormat(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenSourceRepoRepositoryPubsubConfigsServiceAccountEmail(v interface{}, d *schema.ResourceData) interface{} { + return v +} + func expandSourceRepoRepositoryName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return replaceVars(d, config, "projects/{{project}}/repos/{{name}}") } + +func expandSourceRepoRepositoryPubsubConfigs(v interface{}, d TerraformResourceData, config *Config) (map[string]interface{}, error) { + if v == nil { + return map[string]interface{}{}, nil + } + m := make(map[string]interface{}) + for _, raw := range v.(*schema.Set).List() { + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedMessageFormat, err := expandSourceRepoRepositoryPubsubConfigsMessageFormat(original["message_format"], d, config) + if err != nil { + return nil, err + } + transformed["messageFormat"] = transformedMessageFormat + transformedServiceAccountEmail, err := expandSourceRepoRepositoryPubsubConfigsServiceAccountEmail(original["service_account_email"], d, config) + if err != nil { + return nil, err + } + transformed["serviceAccountEmail"] = transformedServiceAccountEmail + + m[original["topic"].(string)] = transformed + } + return m, nil +} + +func expandSourceRepoRepositoryPubsubConfigsMessageFormat(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandSourceRepoRepositoryPubsubConfigsServiceAccountEmail(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func resourceSourceRepoRepositoryUpdateEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + // Add "topic" field using pubsubConfig map key + pubsubConfigsVal := obj["pubsubConfigs"] + if pubsubConfigsVal != nil { + pubsubConfigs := pubsubConfigsVal.(map[string]interface{}) + for key := range pubsubConfigs { + config := pubsubConfigs[key].(map[string]interface{}) + config["topic"] = key + } + } + + // Nest request body in "repo" field + newObj := make(map[string]interface{}) + newObj["repo"] = obj + return newObj, nil +} diff --git a/google/resource_source_repo_repository_generated_test.go b/google/resource_source_repo_repository_generated_test.go index 4ca25e2bfd7..cc2f663ee09 100644 --- a/google/resource_source_repo_repository_generated_test.go +++ b/google/resource_source_repo_repository_generated_test.go @@ -56,6 +56,52 @@ resource "google_sourcerepo_repository" "my-repo" { `, context) } +func TestAccSourceRepoRepository_sourcerepoRepositoryFullExample(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: testAccCheckSourceRepoRepositoryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSourceRepoRepository_sourcerepoRepositoryFullExample(context), + }, + { + ResourceName: "google_sourcerepo_repository.my-repo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccSourceRepoRepository_sourcerepoRepositoryFullExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_service_account" "test-account" { + account_id = "my-account%{random_suffix}" + display_name = "Test Service Account" +} + +resource "google_pubsub_topic" "topic" { + name = "my-topic%{random_suffix}" +} + +resource "google_sourcerepo_repository" "my-repo" { + name = "my-repository%{random_suffix}" + pubsub_configs { + topic = google_pubsub_topic.topic.id + message_format = "JSON" + service_account_email = google_service_account.test-account.email + } +} +`, context) +} + func testAccCheckSourceRepoRepositoryDestroy(s *terraform.State) error { for name, rs := range s.RootModule().Resources { if rs.Type != "google_sourcerepo_repository" { diff --git a/google/resource_sourcerepo_repository_test.go b/google/resource_sourcerepo_repository_test.go index cdb35bb6af4..77f0a4d327e 100644 --- a/google/resource_sourcerepo_repository_test.go +++ b/google/resource_sourcerepo_repository_test.go @@ -29,6 +29,37 @@ func TestAccSourceRepoRepository_basic(t *testing.T) { }) } +func TestAccSourceRepoRepository_update(t *testing.T) { + t.Parallel() + + repositoryName := fmt.Sprintf("source-repo-repository-test-%s", acctest.RandString(10)) + accountId := fmt.Sprintf("account-id-%s", acctest.RandString(10)) + topicName := fmt.Sprintf("topic-name-%s", acctest.RandString(10)) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckSourceRepoRepositoryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSourceRepoRepository_basic(repositoryName), + }, + { + ResourceName: "google_sourcerepo_repository.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccSourceRepoRepository_extended(accountId, topicName, repositoryName), + }, + { + ResourceName: "google_sourcerepo_repository.acceptance", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccSourceRepoRepository_basic(repositoryName string) string { return fmt.Sprintf(` resource "google_sourcerepo_repository" "acceptance" { @@ -36,3 +67,25 @@ resource "google_sourcerepo_repository" "acceptance" { } `, repositoryName) } + +func testAccSourceRepoRepository_extended(accountId string, topicName string, repositoryName string) string { + return fmt.Sprintf(` + resource "google_service_account" "test-account" { + account_id = "%s" + display_name = "Test Service Account" + } + + resource "google_pubsub_topic" "topic" { + name = "%s" + } + + resource "google_sourcerepo_repository" "acceptance" { + name = "%s" + pubsub_configs { + topic = google_pubsub_topic.topic.id + message_format = "JSON" + service_account_email = google_service_account.test-account.email + } + } +`, accountId, topicName, repositoryName) +} diff --git a/website/docs/r/sourcerepo_repository.html.markdown b/website/docs/r/sourcerepo_repository.html.markdown index f2ba47536a7..6837d729750 100644 --- a/website/docs/r/sourcerepo_repository.html.markdown +++ b/website/docs/r/sourcerepo_repository.html.markdown @@ -44,6 +44,33 @@ resource "google_sourcerepo_repository" "my-repo" { name = "my-repository" } ``` +
+ + Open in Cloud Shell + +
+## Example Usage - Sourcerepo Repository Full + + +```hcl +resource "google_service_account" "test-account" { + account_id = "my-account" + display_name = "Test Service Account" +} + +resource "google_pubsub_topic" "topic" { + name = "my-topic" +} + +resource "google_sourcerepo_repository" "my-repo" { + name = "my-repository" + pubsub_configs { + topic = google_pubsub_topic.topic.id + message_format = "JSON" + service_account_email = google_service_account.test-account.email + } +} +``` ## Argument Reference @@ -59,10 +86,32 @@ The following arguments are supported: - - - +* `pubsub_configs` - + (Optional) + How this repository publishes a change in the repository through Cloud Pub/Sub. + Keyed by the topic names. Structure is documented below. + * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +The `pubsub_configs` block supports: + +* `topic` - (Required) The identifier for this object. Format specified above. + +* `message_format` - + (Required) + The format of the Cloud Pub/Sub messages. + - PROTOBUF: The message payload is a serialized protocol buffer of SourceRepoEvent. + - JSON: The message payload is a JSON string of SourceRepoEvent. + +* `service_account_email` - + (Optional) + Email address of the service account used for publishing Cloud Pub/Sub messages. + This service account needs to be in the same project as the PubsubConfig. When added, + the caller needs to have iam.serviceAccounts.actAs permission on this service account. + If unspecified, it defaults to the compute engine default service account. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: @@ -81,6 +130,7 @@ This resource provides the following [Timeouts](/docs/configuration/resources.html#timeouts) configuration options: - `create` - Default is 4 minutes. +- `update` - Default is 4 minutes. - `delete` - Default is 4 minutes. ## Import