diff --git a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb index 1a24ecbc5657..cc5d0742f0e2 100644 --- a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb +++ b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb @@ -290,6 +290,9 @@ var handwrittenResources = map[string]*schema.Resource{ "google_billing_subaccount": resourcemanager.ResourceBillingSubaccount(), "google_cloudfunctions_function": cloudfunctions.ResourceCloudFunctionsFunction(), "google_composer_environment": composer.ResourceComposerEnvironment(), + <% unless version == 'ga' -%> + "google_composer_user_workloads_secret": composer.ResourceComposerUserWorkloadsSecret(), + <% end -%> "google_compute_attached_disk": compute.ResourceComputeAttachedDisk(), "google_compute_instance": compute.ResourceComputeInstance(), "google_compute_disk_async_replication": compute.ResourceComputeDiskAsyncReplication(), diff --git a/mmv1/third_party/terraform/services/composer/resource_composer_user_workloads_secret.go.erb b/mmv1/third_party/terraform/services/composer/resource_composer_user_workloads_secret.go.erb new file mode 100644 index 000000000000..9fd6455e11c8 --- /dev/null +++ b/mmv1/third_party/terraform/services/composer/resource_composer_user_workloads_secret.go.erb @@ -0,0 +1,268 @@ +<% autogen_exception -%> +package composer + +import ( + "fmt" + "log" + "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "github.com/hashicorp/terraform-provider-google/google/verify" + +<% if version == "ga" -%> + "google.golang.org/api/composer/v1" +<% else -%> + composer "google.golang.org/api/composer/v1beta1" +<% end -%> +) + +func ResourceComposerUserWorkloadsSecret() *schema.Resource { + return &schema.Resource{ + Create: resourceComposerUserWorkloadsSecretCreate, + Read: resourceComposerUserWorkloadsSecretRead, + Update: resourceComposerUserWorkloadsSecretUpdate, + Delete: resourceComposerUserWorkloadsSecretDelete, + + Importer: &schema.ResourceImporter{ + State: resourceComposerUserWorkloadsSecretImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(120 * time.Minute), + Update: schema.DefaultTimeout(120 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + CustomizeDiff: customdiff.All( + tpgresource.DefaultProviderProject, + tpgresource.DefaultProviderRegion, + ), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidateGCEName, + Description: `Name of the environment.`, + }, + "region": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + Description: `The location or Compute Engine region for the environment.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: `The ID of the project in which the resource belongs. If it is not provided, the provider project is used.`, + }, + "environment": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidateGCEName, + Description: `Name of the environment.`, + }, + "data": { + Type: schema.TypeMap, + Optional: true, + ForceNew: false, + Sensitive: true, + Description: `A map of the secret data.`, + }, + }, + } +} + +func resourceComposerUserWorkloadsSecretCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + secretName, err := resourceComposerUserWorkloadsSecretName(d, config) + if err != nil { + return err + } + + secret := &composer.UserWorkloadsSecret{ + Name: secretName.ResourceName(), + Data: tpgresource.ConvertStringMap(d.Get("data").(map[string]interface{})), + } + + log.Printf("[DEBUG] Creating new UserWorkloadsSecret %q", secretName.ParentName()) + resp, err := config.NewComposerClient(userAgent).Projects.Locations.Environments.UserWorkloadsSecrets.Create(secretName.ParentName(), secret).Do() + if err != nil { + return fmt.Errorf("Error creating UserWorkloadsSecret: %s", err) + } + + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{region}}/environments/{{environment}}/userWorkloadsSecrets/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + respJson, _ := resp.MarshalJSON() + log.Printf("[DEBUG] Finished creating UserWorkloadsSecret %q: %#v", d.Id(), string(respJson)) + + return resourceComposerUserWorkloadsSecretRead(d, meta) +} + +func resourceComposerUserWorkloadsSecretRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + secretName, err := resourceComposerUserWorkloadsSecretName(d, config) + if err != nil { + return err + } + + res, err := config.NewComposerClient(userAgent).Projects.Locations.Environments.UserWorkloadsSecrets.Get(secretName.ResourceName()).Do() + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("UserWorkloadsSecret %q", d.Id())) + } + + if err := d.Set("project", secretName.Project); err != nil { + return fmt.Errorf("Error setting UserWorkloadsSecret Project: %s", err) + } + if err := d.Set("region", secretName.Region); err != nil { + return fmt.Errorf("Error setting UserWorkloadsSecret Region: %s", err) + } + if err := d.Set("environment", secretName.Environment); err != nil { + return fmt.Errorf("Error setting UserWorkloadsSecret Environment: %s", err) + } + if err := d.Set("name", tpgresource.GetResourceNameFromSelfLink(res.Name)); err != nil { + return fmt.Errorf("Error setting UserWorkloadsSecret Name: %s", err) + } + return nil +} + +func resourceComposerUserWorkloadsSecretUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + secretName, err := resourceComposerUserWorkloadsSecretName(d, config) + if err != nil { + return err + } + + if d.HasChange("data") { + secret := &composer.UserWorkloadsSecret{ + Name: secretName.ResourceName(), + Data: tpgresource.ConvertStringMap(d.Get("data").(map[string]interface{})), + } + + secretJson, _ := secret.MarshalJSON() + log.Printf("[DEBUG] Updating UserWorkloadsSecret %q: %s", d.Id(), string(secretJson)) + + resp, err := config.NewComposerClient(userAgent).Projects.Locations.Environments.UserWorkloadsSecrets.Update(secretName.ResourceName(), secret).Do() + if err != nil { + return err + } + + respJson, _ := resp.MarshalJSON() + log.Printf("[DEBUG] Finished updating UserWorkloadsSecret %q: %s", d.Id(), string(respJson)) + } + + return nil +} + +func resourceComposerUserWorkloadsSecretDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + secretName, err := resourceComposerUserWorkloadsSecretName(d, config) + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting UserWorkloadsSecret %q", d.Id()) + _, err = config.NewComposerClient(userAgent).Projects.Locations.Environments.UserWorkloadsSecrets.Delete(secretName.ResourceName()).Do() + if err != nil { + return err + } + log.Printf("[DEBUG] Finished deleting UserWorkloadsSecret %q", d.Id()) + + return nil +} + +func resourceComposerUserWorkloadsSecretImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{"projects/(?P[^/]+)/locations/(?P[^/]+)/environments/(?P[^/]+)/userWorkloadsSecrets/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{region}}/environments/{{environment}}/userWorkloadsSecrets/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // retrieve "data" in advance, because Read function won't do it. + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return nil, err + } + + res, err := config.NewComposerClient(userAgent).Projects.Locations.Environments.UserWorkloadsSecrets.Get(id).Do() + if err != nil { + return nil, err + } + + if err := d.Set("data", res.Data); err != nil { + return nil, fmt.Errorf("Error setting UserWorkloadsSecret Data: %s", err) + } + + return []*schema.ResourceData{d}, nil +} + +func resourceComposerUserWorkloadsSecretName(d *schema.ResourceData, config *transport_tpg.Config) (*UserWorkloadsSecretsName, error) { + project, err := tpgresource.GetProject(d, config) + if err != nil { + return nil, err + } + + region, err := tpgresource.GetRegion(d, config) + if err != nil { + return nil, err + } + + return &UserWorkloadsSecretsName{ + Project: project, + Region: region, + Environment: d.Get("environment").(string), + Secret: d.Get("name").(string), + }, nil +} + +type UserWorkloadsSecretsName struct { + Project string + Region string + Environment string + Secret string +} + +func (n *UserWorkloadsSecretsName) ResourceName() string { + return fmt.Sprintf("projects/%s/locations/%s/environments/%s/userWorkloadsSecrets/%s", n.Project, n.Region, n.Environment, n.Secret) +} + +func (n *UserWorkloadsSecretsName) ParentName() string { + return fmt.Sprintf("projects/%s/locations/%s/environments/%s", n.Project, n.Region, n.Environment) +} diff --git a/mmv1/third_party/terraform/services/composer/resource_composer_user_workloads_secret_test.go.erb b/mmv1/third_party/terraform/services/composer/resource_composer_user_workloads_secret_test.go.erb new file mode 100644 index 000000000000..b7f708e0618e --- /dev/null +++ b/mmv1/third_party/terraform/services/composer/resource_composer_user_workloads_secret_test.go.erb @@ -0,0 +1,178 @@ +<% autogen_exception -%> +package composer_test + +import ( + "fmt" + "testing" + "strings" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/services/composer" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const testComposerUserWorkloadsSecretPrefix = "tf-test-composer-secret" + +func TestAccComposerUserWorkloadsSecret_basic(t *testing.T) { + t.Parallel() + + envName := fmt.Sprintf("%s-%d", testComposerEnvironmentPrefix, acctest.RandInt(t)) + secretName := fmt.Sprintf("%s-%d", testComposerUserWorkloadsSecretPrefix, acctest.RandInt(t)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + // CheckDestroy: testAccComposerUserWorkloadsSecretDestroy(t), + Steps: []resource.TestStep{ + { + Config: testAccComposerUserWorkloadsSecret_basic(envName, secretName, envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_composer_user_workloads_secret.test", "data.username"), + resource.TestCheckResourceAttrSet("google_composer_user_workloads_secret.test", "data.password"), + ), + }, + { + ResourceName: "google_composer_user_workloads_secret.test", + ImportState: true, + }, + }, + }) +} + +func TestAccComposerUserWorkloadsSecret_update(t *testing.T) { + t.Parallel() + + envName := fmt.Sprintf("%s-%d", testComposerEnvironmentPrefix, acctest.RandInt(t)) + secretName := fmt.Sprintf("%s-%d", testComposerUserWorkloadsSecretPrefix, acctest.RandInt(t)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccComposerUserWorkloadsSecret_basic(envName, secretName, envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv()), + }, + { + Config: testAccComposerUserWorkloadsSecret_update(envName, secretName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_composer_user_workloads_secret.test", "data.email"), + resource.TestCheckResourceAttrSet("google_composer_user_workloads_secret.test", "data.password"), + resource.TestCheckNoResourceAttr("google_composer_user_workloads_secret.test", "data.username"), + ), + }, + }, + }) +} + +func TestAccComposerUserWorkloadsSecret_delete(t *testing.T) { + t.Parallel() + + envName := fmt.Sprintf("%s-%d", testComposerEnvironmentPrefix, acctest.RandInt(t)) + secretName := fmt.Sprintf("%s-%d", testComposerUserWorkloadsSecretPrefix, acctest.RandInt(t)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccComposerUserWorkloadsSecret_basic(envName, secretName, envvar.GetTestProjectFromEnv(), envvar.GetTestRegionFromEnv()), + }, + { + Config: testAccComposerUserWorkloadsSecret_delete(envName), + Check: resource.ComposeTestCheckFunc( + testAccComposerUserWorkloadsSecretDestroyed(t), + ), + }, + }, + }) +} + +func testAccComposerUserWorkloadsSecret_basic(envName, secretName, project, region string) string { + return fmt.Sprintf(` +resource "google_composer_environment" "test" { + name = "%s" + config { + software_config { + image_version = "composer-3-airflow-2" + } + } +} +resource "google_composer_user_workloads_secret" "test" { + environment = google_composer_environment.test.name + name = "%s" + project = "%s" + region = "%s" + data = { + username: base64encode("username"), + password: base64encode("password"), + } +} +`, envName, secretName, project, region) +} + +func testAccComposerUserWorkloadsSecret_update(envName, secretName string) string { + return fmt.Sprintf(` +resource "google_composer_environment" "test" { + name = "%s" + config { + software_config { + image_version = "composer-3-airflow-2" + } + } +} +resource "google_composer_user_workloads_secret" "test" { + environment = google_composer_environment.test.name + name = "%s" + data = { + email: base64encode("email"), + password: base64encode("password"), + } +} +`, envName, secretName) +} + +func testAccComposerUserWorkloadsSecret_delete(envName string) string { + return fmt.Sprintf(` +resource "google_composer_environment" "test" { + name = "%s" + config { + software_config { + image_version = "composer-3-airflow-2" + } + } +} +`, envName) +} + +func testAccComposerUserWorkloadsSecretDestroyed(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_composer_user_workloads_secret" { + continue + } + + idTokens := strings.Split(rs.Primary.ID, "/") + if len(idTokens) != 8 { + return fmt.Errorf("Invalid ID %q, expected format projects/{project}/regions/{region}/environments/{environment}/userWorkloadsSecrets/{name}", rs.Primary.ID) + } + secretName := &composer.UserWorkloadsSecretsName{ + Project: idTokens[1], + Region: idTokens[3], + Environment: idTokens[5], + Secret: idTokens[7], + } + + _, err := config.NewComposerClient(config.UserAgent).Projects.Locations.Environments.UserWorkloadsSecrets.Get(secretName.ResourceName()).Do() + if err == nil { + return fmt.Errorf("secret %s still exists", secretName.ResourceName()) + } + } + + return nil + } +} diff --git a/mmv1/third_party/terraform/website/docs/r/composer_user_workloads_secret.html.markdown b/mmv1/third_party/terraform/website/docs/r/composer_user_workloads_secret.html.markdown new file mode 100644 index 000000000000..6e40cd68e745 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/r/composer_user_workloads_secret.html.markdown @@ -0,0 +1,99 @@ +--- +subcategory: "Cloud Composer" +description: |- + Kubernetes secret in Composer Environment workloads. +--- + +# google\_composer\_user\_workloads\_secret + +Provides a way to manage Kubernetes secret in Composer Environment workloads. + +## Example Usage + +```hcl +resource "google_composer_environment" "example" { + name = "example-environment" + project = "example-project" + region = "us-central1" + config { + software_config { + image_version = "example-image-version" + } + } +} + +resource "google_composer_user_workloads_secret" "example" { + name = "example-secret" + project = "example-project" + region = "us-central1" + environment = google_composer_environment.example.name + data = { + email: base64encode("example-email"), + password: base64encode("example-password"), + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - + (Required) + Name of the Secret. + +* `region` - + (Optional) + The location or Compute Engine region for the environment. + +* `project` - + (Optional) + The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + +* `environment` - + Environment where the secret will be stored and used. + +* `data` - + (Optional) + The "data" field of Kubernetes Secret, organized in key-value pairs, + which can contain sensitive values such as a password, a token, or a key. + Content of this field will not be displayed in CLI output, + but it will be stored in terraform state file. To protect sensitive data, + follow the best practices outlined in the HashiCorp documentation: + https://developer.hashicorp.com/terraform/language/state/sensitive-data. + The values for all keys have to be base64-encoded strings. + For details see: https://kubernetes.io/docs/concepts/configuration/secret/ + + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `projects/{{project}}/locations/{{region}}/environments/{{environment}}/userWorkloadsSecrets/{{name}}` + +## Import + +Secret can be imported using any of these accepted formats: + +* `projects/{{project}}/locations/{{region}}/environments/{{environment}}/userWorkloadsSecrets/{{name}}` +* `{{project}}/{{region}}/{{environment}}/{{name}}` +* `{{name}}` + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import User Workloads Secret using one of the formats above. For example: + +```tf +import { + id = "projects/{{project}}/locations/{{region}}/environments/{{environment}}/userWorkloadsSecrets/{{name}}" + to = google_composer_user_workloads_secret.example +} +``` + +When using the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), Environment can be imported using one of the formats above. For example: + +``` +$ terraform import google_composer_user_workloads_secret.example projects/{{project}}/locations/{{region}}/environments/{{environment}}/userWorkloadsSecrets/{{name}} +$ terraform import google_composer_user_workloads_secret.example {{project}}/{{region}}/{{environment}}/{{name}} +$ terraform import google_composer_user_workloads_secret.example {{name}} +```