diff --git a/.changelog/6020.txt b/.changelog/6020.txt new file mode 100644 index 00000000000..593741baf7c --- /dev/null +++ b/.changelog/6020.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +bigquery connection: Added connection of type cloud_resource for `google_bigquery_connection` +``` +```release-note:enhancement +bigquery connection: Made `google_bigquery_connection` part of GA terraform provider. +``` diff --git a/google/config.go b/google/config.go index 18d09d9aa17..efae8f6bebc 100644 --- a/google/config.go +++ b/google/config.go @@ -166,6 +166,7 @@ type Config struct { ApigeeBasePath string AppEngineBasePath string BigQueryBasePath string + BigqueryConnectionBasePath string BigqueryDataTransferBasePath string BigqueryReservationBasePath string BigtableBasePath string @@ -267,6 +268,7 @@ const ActiveDirectoryBasePathKey = "ActiveDirectory" const ApigeeBasePathKey = "Apigee" const AppEngineBasePathKey = "AppEngine" const BigQueryBasePathKey = "BigQuery" +const BigqueryConnectionBasePathKey = "BigqueryConnection" const BigqueryDataTransferBasePathKey = "BigqueryDataTransfer" const BigqueryReservationBasePathKey = "BigqueryReservation" const BigtableBasePathKey = "Bigtable" @@ -350,6 +352,7 @@ var DefaultBasePaths = map[string]string{ ApigeeBasePathKey: "https://apigee.googleapis.com/v1/", AppEngineBasePathKey: "https://appengine.googleapis.com/v1/", BigQueryBasePathKey: "https://bigquery.googleapis.com/bigquery/v2/", + BigqueryConnectionBasePathKey: "https://bigqueryconnection.googleapis.com/v1/", BigqueryDataTransferBasePathKey: "https://bigquerydatatransfer.googleapis.com/v1/", BigqueryReservationBasePathKey: "https://bigqueryreservation.googleapis.com/v1/", BigtableBasePathKey: "https://bigtableadmin.googleapis.com/v2/", @@ -1195,6 +1198,7 @@ func ConfigureBasePaths(c *Config) { c.ApigeeBasePath = DefaultBasePaths[ApigeeBasePathKey] c.AppEngineBasePath = DefaultBasePaths[AppEngineBasePathKey] c.BigQueryBasePath = DefaultBasePaths[BigQueryBasePathKey] + c.BigqueryConnectionBasePath = DefaultBasePaths[BigqueryConnectionBasePathKey] c.BigqueryDataTransferBasePath = DefaultBasePaths[BigqueryDataTransferBasePathKey] c.BigqueryReservationBasePath = DefaultBasePaths[BigqueryReservationBasePathKey] c.BigtableBasePath = DefaultBasePaths[BigtableBasePathKey] diff --git a/google/provider.go b/google/provider.go index b918a73bf3d..48b85647a66 100644 --- a/google/provider.go +++ b/google/provider.go @@ -197,6 +197,14 @@ func Provider() *schema.Provider { "GOOGLE_BIG_QUERY_CUSTOM_ENDPOINT", }, DefaultBasePaths[BigQueryBasePathKey]), }, + "bigquery_connection_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCustomEndpoint, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_BIGQUERY_CONNECTION_CUSTOM_ENDPOINT", + }, DefaultBasePaths[BigqueryConnectionBasePathKey]), + }, "bigquery_data_transfer_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -839,9 +847,9 @@ func Provider() *schema.Provider { return provider } -// Generated resources: 219 +// Generated resources: 220 // Generated IAM resources: 108 -// Total generated resources: 327 +// Total generated resources: 328 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -890,6 +898,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_bigquery_table_iam_member": ResourceIamMember(BigQueryTableIamSchema, BigQueryTableIamUpdaterProducer, BigQueryTableIdParseFunc), "google_bigquery_table_iam_policy": ResourceIamPolicy(BigQueryTableIamSchema, BigQueryTableIamUpdaterProducer, BigQueryTableIdParseFunc), "google_bigquery_routine": resourceBigQueryRoutine(), + "google_bigquery_connection": resourceBigqueryConnectionConnection(), "google_bigquery_data_transfer_config": resourceBigqueryDataTransferConfig(), "google_bigquery_reservation": resourceBigqueryReservationReservation(), "google_bigtable_app_profile": resourceBigtableAppProfile(), @@ -1428,6 +1437,7 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.ApigeeBasePath = d.Get("apigee_custom_endpoint").(string) config.AppEngineBasePath = d.Get("app_engine_custom_endpoint").(string) config.BigQueryBasePath = d.Get("big_query_custom_endpoint").(string) + config.BigqueryConnectionBasePath = d.Get("bigquery_connection_custom_endpoint").(string) config.BigqueryDataTransferBasePath = d.Get("bigquery_data_transfer_custom_endpoint").(string) config.BigqueryReservationBasePath = d.Get("bigquery_reservation_custom_endpoint").(string) config.BigtableBasePath = d.Get("bigtable_custom_endpoint").(string) diff --git a/google/resource_bigquery_connection.go b/google/resource_bigquery_connection.go new file mode 100644 index 00000000000..a6c23a44c6f --- /dev/null +++ b/google/resource_bigquery_connection.go @@ -0,0 +1,667 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules 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 ( + "fmt" + "log" + "reflect" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceBigqueryConnectionConnection() *schema.Resource { + return &schema.Resource{ + Create: resourceBigqueryConnectionConnectionCreate, + Read: resourceBigqueryConnectionConnectionRead, + Update: resourceBigqueryConnectionConnectionUpdate, + Delete: resourceBigqueryConnectionConnectionDelete, + + Importer: &schema.ResourceImporter{ + State: resourceBigqueryConnectionConnectionImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "cloud_resource": { + Type: schema.TypeList, + Optional: true, + Description: `Cloud Resource properties.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "service_account_id": { + Type: schema.TypeString, + Computed: true, + Description: `The account ID of the service created for the purpose of this connection.`, + }, + }, + }, + ExactlyOneOf: []string{"cloud_sql", "cloud_resource"}, + }, + "cloud_sql": { + Type: schema.TypeList, + Optional: true, + Description: `Cloud SQL properties.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "credential": { + Type: schema.TypeList, + Required: true, + Description: `Cloud SQL properties.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "password": { + Type: schema.TypeString, + Required: true, + Description: `Password for database.`, + Sensitive: true, + }, + "username": { + Type: schema.TypeString, + Required: true, + Description: `Username for database.`, + }, + }, + }, + }, + "database": { + Type: schema.TypeString, + Required: true, + Description: `Database name.`, + }, + "instance_id": { + Type: schema.TypeString, + Required: true, + Description: `Cloud SQL instance ID in the form project:location:instance.`, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateEnum([]string{"DATABASE_TYPE_UNSPECIFIED", "POSTGRES", "MYSQL"}), + Description: `Type of the Cloud SQL database. Possible values: ["DATABASE_TYPE_UNSPECIFIED", "POSTGRES", "MYSQL"]`, + }, + }, + }, + ExactlyOneOf: []string{"cloud_sql", "cloud_resource"}, + }, + "connection_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + Description: `Optional connection id that should be assigned to the created connection.`, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: `A descriptive description for the connection`, + }, + "friendly_name": { + Type: schema.TypeString, + Optional: true, + Description: `A descriptive name for the connection`, + }, + "location": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `The geographic location where the connection should reside. +Cloud SQL instance must be in the same location as the connection +with following exceptions: Cloud SQL us-central1 maps to BigQuery US, Cloud SQL europe-west1 maps to BigQuery EU. +Examples: US, EU, asia-northeast1, us-central1, europe-west1. The default value is US.`, + Default: "US", + }, + "has_credential": { + Type: schema.TypeBool, + Computed: true, + Description: `True if the connection has credential assigned.`, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The resource name of the connection in the form of: +"projects/{project_id}/locations/{location_id}/connections/{connectionId}"`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + UseJSONNumber: true, + } +} + +func resourceBigqueryConnectionConnectionCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + connection_idProp, err := expandBigqueryConnectionConnectionConnectionId(d.Get("connection_id"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("connection_id"); !isEmptyValue(reflect.ValueOf(connection_idProp)) && (ok || !reflect.DeepEqual(v, connection_idProp)) { + obj["connection_id"] = connection_idProp + } + friendlyNameProp, err := expandBigqueryConnectionConnectionFriendlyName(d.Get("friendly_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("friendly_name"); !isEmptyValue(reflect.ValueOf(friendlyNameProp)) && (ok || !reflect.DeepEqual(v, friendlyNameProp)) { + obj["friendlyName"] = friendlyNameProp + } + descriptionProp, err := expandBigqueryConnectionConnectionDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + cloudSqlProp, err := expandBigqueryConnectionConnectionCloudSql(d.Get("cloud_sql"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("cloud_sql"); !isEmptyValue(reflect.ValueOf(cloudSqlProp)) && (ok || !reflect.DeepEqual(v, cloudSqlProp)) { + obj["cloudSql"] = cloudSqlProp + } + cloudResourceProp, err := expandBigqueryConnectionConnectionCloudResource(d.Get("cloud_resource"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("cloud_resource"); ok || !reflect.DeepEqual(v, cloudResourceProp) { + obj["cloudResource"] = cloudResourceProp + } + + obj, err = resourceBigqueryConnectionConnectionEncoder(d, meta, obj) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{BigqueryConnectionBasePath}}projects/{{project}}/locations/{{location}}/connections?connectionId={{connection_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Connection: %#v", obj) + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Connection: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Connection: %s", err) + } + if err := d.Set("name", flattenBigqueryConnectionConnectionName(res["name"], d, config)); err != nil { + return fmt.Errorf(`Error setting computed identity field "name": %s`, err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + if isEmptyValue(reflect.ValueOf(d.Get("connection_id"))) { + // connection id is set by API when unset and required to GET the connection + // it is set by reading the "name" field rather than a field in the response + if err := d.Set("connection_id", flattenBigqueryConnectionConnectionConnectionId("", d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + } + + // Reset id to make sure connection_id is not empty + id2, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id2) + + log.Printf("[DEBUG] Finished creating Connection %q: %#v", d.Id(), res) + + return resourceBigqueryConnectionConnectionRead(d, meta) +} + +func resourceBigqueryConnectionConnectionRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{BigqueryConnectionBasePath}}projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Connection: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "GET", billingProject, url, userAgent, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("BigqueryConnectionConnection %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + + if err := d.Set("name", flattenBigqueryConnectionConnectionName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + if err := d.Set("connection_id", flattenBigqueryConnectionConnectionConnectionId(res["connection_id"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + if err := d.Set("friendly_name", flattenBigqueryConnectionConnectionFriendlyName(res["friendlyName"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + if err := d.Set("description", flattenBigqueryConnectionConnectionDescription(res["description"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + if err := d.Set("has_credential", flattenBigqueryConnectionConnectionHasCredential(res["hasCredential"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + if err := d.Set("cloud_sql", flattenBigqueryConnectionConnectionCloudSql(res["cloudSql"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + if err := d.Set("cloud_resource", flattenBigqueryConnectionConnectionCloudResource(res["cloudResource"], d, config)); err != nil { + return fmt.Errorf("Error reading Connection: %s", err) + } + + return nil +} + +func resourceBigqueryConnectionConnectionUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Connection: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + friendlyNameProp, err := expandBigqueryConnectionConnectionFriendlyName(d.Get("friendly_name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("friendly_name"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, friendlyNameProp)) { + obj["friendlyName"] = friendlyNameProp + } + descriptionProp, err := expandBigqueryConnectionConnectionDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + cloudSqlProp, err := expandBigqueryConnectionConnectionCloudSql(d.Get("cloud_sql"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("cloud_sql"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, cloudSqlProp)) { + obj["cloudSql"] = cloudSqlProp + } + cloudResourceProp, err := expandBigqueryConnectionConnectionCloudResource(d.Get("cloud_resource"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("cloud_resource"); ok || !reflect.DeepEqual(v, cloudResourceProp) { + obj["cloudResource"] = cloudResourceProp + } + + obj, err = resourceBigqueryConnectionConnectionEncoder(d, meta, obj) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{BigqueryConnectionBasePath}}projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Connection %q: %#v", d.Id(), obj) + updateMask := []string{} + + if d.HasChange("friendly_name") { + updateMask = append(updateMask, "friendlyName") + } + + if d.HasChange("description") { + updateMask = append(updateMask, "description") + } + + if d.HasChange("cloud_sql") { + updateMask = append(updateMask, "cloudSql") + } + + if d.HasChange("cloud_resource") { + updateMask = append(updateMask, "cloudResource") + } + // 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 == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Connection %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Connection %q: %#v", d.Id(), res) + } + + return resourceBigqueryConnectionConnectionRead(d, meta) +} + +func resourceBigqueryConnectionConnectionDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Connection: %s", err) + } + billingProject = project + + url, err := replaceVars(d, config, "{{BigqueryConnectionBasePath}}projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Connection %q", d.Id()) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "DELETE", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Connection") + } + + log.Printf("[DEBUG] Finished deleting Connection %q: %#v", d.Id(), res) + return nil +} + +func resourceBigqueryConnectionConnectionImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/locations/(?P[^/]+)/connections/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenBigqueryConnectionConnectionName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionConnectionId(v interface{}, d *schema.ResourceData, config *Config) interface{} { + parts := strings.Split(d.Get("name").(string), "/") + return parts[len(parts)-1] +} + +func flattenBigqueryConnectionConnectionFriendlyName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionDescription(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionHasCredential(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionCloudSql(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["instance_id"] = + flattenBigqueryConnectionConnectionCloudSqlInstanceId(original["instanceId"], d, config) + transformed["database"] = + flattenBigqueryConnectionConnectionCloudSqlDatabase(original["database"], d, config) + transformed["credential"] = + flattenBigqueryConnectionConnectionCloudSqlCredential(original["credential"], d, config) + transformed["type"] = + flattenBigqueryConnectionConnectionCloudSqlType(original["type"], d, config) + return []interface{}{transformed} +} +func flattenBigqueryConnectionConnectionCloudSqlInstanceId(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionCloudSqlDatabase(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionCloudSqlCredential(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return []interface{}{ + map[string]interface{}{ + "username": d.Get("cloud_sql.0.credential.0.username"), + "password": d.Get("cloud_sql.0.credential.0.password"), + }, + } +} + +func flattenBigqueryConnectionConnectionCloudSqlType(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenBigqueryConnectionConnectionCloudResource(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["service_account_id"] = + flattenBigqueryConnectionConnectionCloudResourceServiceAccountId(original["serviceAccountId"], d, config) + return []interface{}{transformed} +} +func flattenBigqueryConnectionConnectionCloudResourceServiceAccountId(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func expandBigqueryConnectionConnectionConnectionId(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionFriendlyName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionCloudSql(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedInstanceId, err := expandBigqueryConnectionConnectionCloudSqlInstanceId(original["instance_id"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedInstanceId); val.IsValid() && !isEmptyValue(val) { + transformed["instanceId"] = transformedInstanceId + } + + transformedDatabase, err := expandBigqueryConnectionConnectionCloudSqlDatabase(original["database"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedDatabase); val.IsValid() && !isEmptyValue(val) { + transformed["database"] = transformedDatabase + } + + transformedCredential, err := expandBigqueryConnectionConnectionCloudSqlCredential(original["credential"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedCredential); val.IsValid() && !isEmptyValue(val) { + transformed["credential"] = transformedCredential + } + + transformedType, err := expandBigqueryConnectionConnectionCloudSqlType(original["type"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedType); val.IsValid() && !isEmptyValue(val) { + transformed["type"] = transformedType + } + + return transformed, nil +} + +func expandBigqueryConnectionConnectionCloudSqlInstanceId(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionCloudSqlDatabase(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionCloudSqlCredential(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedUsername, err := expandBigqueryConnectionConnectionCloudSqlCredentialUsername(original["username"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedUsername); val.IsValid() && !isEmptyValue(val) { + transformed["username"] = transformedUsername + } + + transformedPassword, err := expandBigqueryConnectionConnectionCloudSqlCredentialPassword(original["password"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPassword); val.IsValid() && !isEmptyValue(val) { + transformed["password"] = transformedPassword + } + + return transformed, nil +} + +func expandBigqueryConnectionConnectionCloudSqlCredentialUsername(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionCloudSqlCredentialPassword(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionCloudSqlType(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBigqueryConnectionConnectionCloudResource(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedServiceAccountId, err := expandBigqueryConnectionConnectionCloudResourceServiceAccountId(original["service_account_id"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedServiceAccountId); val.IsValid() && !isEmptyValue(val) { + transformed["serviceAccountId"] = transformedServiceAccountId + } + + return transformed, nil +} + +func expandBigqueryConnectionConnectionCloudResourceServiceAccountId(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func resourceBigqueryConnectionConnectionEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + // connection_id is needed to qualify the URL but cannot be sent in the body + delete(obj, "connection_id") + return obj, nil +} diff --git a/google/resource_bigquery_connection_generated_test.go b/google/resource_bigquery_connection_generated_test.go new file mode 100644 index 00000000000..92c863ce18b --- /dev/null +++ b/google/resource_bigquery_connection_generated_test.go @@ -0,0 +1,252 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules 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 ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccBigqueryConnectionConnection_bigqueryConnectionBasicExample(t *testing.T) { + skipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "deletion_protection": false, + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + "time": {}, + }, + CheckDestroy: testAccCheckBigqueryConnectionConnectionDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigqueryConnectionConnection_bigqueryConnectionBasicExample(context), + }, + { + ResourceName: "google_bigquery_connection.connection", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location", "cloud_sql.0.credential"}, + }, + }, + }) +} + +func testAccBigqueryConnectionConnection_bigqueryConnectionBasicExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_sql_database_instance" "instance" { + name = "tf-test-my-database-instance%{random_suffix}" + database_version = "POSTGRES_11" + region = "us-central1" + settings { + tier = "db-f1-micro" + } + + deletion_protection = "%{deletion_protection}" +} + +resource "google_sql_database" "db" { + instance = google_sql_database_instance.instance.name + name = "db" +} + +resource "random_password" "pwd" { + length = 16 + special = false +} + +resource "google_sql_user" "user" { + name = "user%{random_suffix}" + instance = google_sql_database_instance.instance.name + password = random_password.pwd.result +} + +resource "google_bigquery_connection" "connection" { + friendly_name = "👋" + description = "a riveting description" + cloud_sql { + instance_id = google_sql_database_instance.instance.connection_name + database = google_sql_database.db.name + type = "POSTGRES" + credential { + username = google_sql_user.user.name + password = google_sql_user.user.password + } + } +} +`, context) +} + +func TestAccBigqueryConnectionConnection_bigqueryConnectionFullExample(t *testing.T) { + skipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "deletion_protection": false, + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + "time": {}, + }, + CheckDestroy: testAccCheckBigqueryConnectionConnectionDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigqueryConnectionConnection_bigqueryConnectionFullExample(context), + }, + { + ResourceName: "google_bigquery_connection.connection", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location", "cloud_sql.0.credential"}, + }, + }, + }) +} + +func testAccBigqueryConnectionConnection_bigqueryConnectionFullExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_sql_database_instance" "instance" { + name = "tf-test-my-database-instance%{random_suffix}" + database_version = "POSTGRES_11" + region = "us-central1" + settings { + tier = "db-f1-micro" + } + + deletion_protection = "%{deletion_protection}" +} + +resource "google_sql_database" "db" { + instance = google_sql_database_instance.instance.name + name = "db" +} + +resource "random_password" "pwd" { + length = 16 + special = false +} + +resource "google_sql_user" "user" { + name = "user%{random_suffix}" + instance = google_sql_database_instance.instance.name + password = random_password.pwd.result +} + +resource "google_bigquery_connection" "connection" { + connection_id = "tf-test-my-connection%{random_suffix}" + location = "US" + friendly_name = "👋" + description = "a riveting description" + cloud_sql { + instance_id = google_sql_database_instance.instance.connection_name + database = google_sql_database.db.name + type = "POSTGRES" + credential { + username = google_sql_user.user.name + password = google_sql_user.user.password + } + } +} +`, context) +} + +func TestAccBigqueryConnectionConnection_bigqueryConnectionCloudResourceExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": {}, + "time": {}, + }, + CheckDestroy: testAccCheckBigqueryConnectionConnectionDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigqueryConnectionConnection_bigqueryConnectionCloudResourceExample(context), + }, + { + ResourceName: "google_bigquery_connection.connection", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, + }, + }, + }) +} + +func testAccBigqueryConnectionConnection_bigqueryConnectionCloudResourceExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_bigquery_connection" "connection" { + connection_id = "tf-test-my-connection%{random_suffix}" + location = "US" + friendly_name = "👋" + description = "a riveting description" + cloud_resource {} +} +`, context) +} + +func testAccCheckBigqueryConnectionConnectionDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_bigquery_connection" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := googleProviderConfig(t) + + url, err := replaceVarsForTest(config, rs, "{{BigqueryConnectionBasePath}}projects/{{project}}/locations/{{location}}/connections/{{connection_id}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = sendRequest(config, "GET", billingProject, url, config.userAgent, nil) + if err == nil { + return fmt.Errorf("BigqueryConnectionConnection still exists at %s", url) + } + } + + return nil + } +} diff --git a/google/resource_bigquery_connection_sweeper_test.go b/google/resource_bigquery_connection_sweeper_test.go new file mode 100644 index 00000000000..086ecedbc98 --- /dev/null +++ b/google/resource_bigquery_connection_sweeper_test.go @@ -0,0 +1,128 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules 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 ( + "context" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func init() { + resource.AddTestSweepers("BigqueryConnectionConnection", &resource.Sweeper{ + Name: "BigqueryConnectionConnection", + F: testSweepBigqueryConnectionConnection, + }) +} + +// At the time of writing, the CI only passes us-central1 as the region +func testSweepBigqueryConnectionConnection(region string) error { + resourceName := "BigqueryConnectionConnection" + log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName) + + config, err := sharedConfigForRegion(region) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err) + return err + } + + err = config.LoadAndValidate(context.Background()) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err) + return err + } + + t := &testing.T{} + billingId := getTestBillingAccountFromEnv(t) + + // Setup variables to replace in list template + d := &ResourceDataMock{ + FieldsInSchema: map[string]interface{}{ + "project": config.Project, + "region": region, + "location": region, + "zone": "-", + "billing_account": billingId, + }, + } + + listTemplate := strings.Split("https://bigqueryconnection.googleapis.com/v1/projects/{{project}}/locations/{{location}}/connections", "?")[0] + listUrl, err := replaceVars(d, config, listTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err) + return nil + } + + res, err := sendRequest(config, "GET", config.Project, listUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) + return nil + } + + resourceList, ok := res["connections"] + if !ok { + log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.") + return nil + } + + rl := resourceList.([]interface{}) + + log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName) + // Keep count of items that aren't sweepable for logging. + nonPrefixCount := 0 + for _, ri := range rl { + obj := ri.(map[string]interface{}) + var name string + // Id detected in the delete URL, attempt to use id. + if obj["id"] != nil { + name = GetResourceNameFromSelfLink(obj["id"].(string)) + } else if obj["name"] != nil { + name = GetResourceNameFromSelfLink(obj["name"].(string)) + } else { + log.Printf("[INFO][SWEEPER_LOG] %s resource name and id were nil", resourceName) + return nil + } + // Skip resources that shouldn't be sweeped + if !isSweepableTestResource(name) { + nonPrefixCount++ + continue + } + + deleteTemplate := "https://bigqueryconnection.googleapis.com/v1/projects/{{project}}/locations/{{location}}/connections/{{connection_id}}" + deleteUrl, err := replaceVars(d, config, deleteTemplate) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err) + return nil + } + deleteUrl = deleteUrl + name + + // Don't wait on operations as we may have a lot to delete + _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, config.userAgent, nil) + if err != nil { + log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err) + } else { + log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name) + } + } + + if nonPrefixCount > 0 { + log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount) + } + + return nil +} diff --git a/google/resource_clouddeploy_delivery_pipeline_generated_test.go b/google/resource_clouddeploy_delivery_pipeline_generated_test.go index 01fc5b4975d..95b74ca2cfe 100644 --- a/google/resource_clouddeploy_delivery_pipeline_generated_test.go +++ b/google/resource_clouddeploy_delivery_pipeline_generated_test.go @@ -114,9 +114,9 @@ resource "google_clouddeploy_delivery_pipeline" "primary" { description = "updated description" labels = { - my_second_label = "updated-example-label-2" - my_third_label = "example-label-3" + + my_second_label = "updated-example-label-2" } project = "%{project_name}" diff --git a/google/resource_clouddeploy_target_generated_test.go b/google/resource_clouddeploy_target_generated_test.go index 1a9eff94bf6..f0f7c9e77e5 100644 --- a/google/resource_clouddeploy_target_generated_test.go +++ b/google/resource_clouddeploy_target_generated_test.go @@ -91,9 +91,9 @@ resource "google_clouddeploy_target" "primary" { name = "tf-test-target%{random_suffix}" annotations = { - my_first_annotation = "example-annotation-1" - my_second_annotation = "example-annotation-2" + + my_first_annotation = "example-annotation-1" } description = "basic description" @@ -123,9 +123,9 @@ resource "google_clouddeploy_target" "primary" { name = "tf-test-target%{random_suffix}" annotations = { - my_third_annotation = "example-annotation-3" - my_second_annotation = "updated-example-annotation-2" + + my_third_annotation = "example-annotation-3" } description = "updated description" @@ -175,9 +175,9 @@ resource "google_clouddeploy_target" "primary" { } labels = { - my_second_label = "updated-example-label-2" - my_third_label = "example-label-3" + + my_second_label = "updated-example-label-2" } project = "%{project_name}" diff --git a/website/docs/r/bigquery_connection.html.markdown b/website/docs/r/bigquery_connection.html.markdown index 58e37ef18ac..5225b6a5eb1 100644 --- a/website/docs/r/bigquery_connection.html.markdown +++ b/website/docs/r/bigquery_connection.html.markdown @@ -24,8 +24,6 @@ description: |- A connection allows BigQuery connections to external data sources.. -~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. -See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. To get more information about Connection, see: @@ -46,7 +44,6 @@ state as plain-text. [Read more about sensitive data in state](/language/state/s ```hcl resource "google_sql_database_instance" "instance" { - provider = google-beta name = "my-database-instance" database_version = "POSTGRES_11" region = "us-central1" @@ -58,7 +55,6 @@ resource "google_sql_database_instance" "instance" { } resource "google_sql_database" "db" { - provider = google-beta instance = google_sql_database_instance.instance.name name = "db" } @@ -69,14 +65,12 @@ resource "random_password" "pwd" { } resource "google_sql_user" "user" { - provider = google-beta name = "user" instance = google_sql_database_instance.instance.name password = random_password.pwd.result } resource "google_bigquery_connection" "connection" { - provider = google-beta friendly_name = "👋" description = "a riveting description" cloud_sql { @@ -100,7 +94,6 @@ resource "google_bigquery_connection" "connection" { ```hcl resource "google_sql_database_instance" "instance" { - provider = google-beta name = "my-database-instance" database_version = "POSTGRES_11" region = "us-central1" @@ -112,7 +105,6 @@ resource "google_sql_database_instance" "instance" { } resource "google_sql_database" "db" { - provider = google-beta instance = google_sql_database_instance.instance.name name = "db" } @@ -123,14 +115,12 @@ resource "random_password" "pwd" { } resource "google_sql_user" "user" { - provider = google-beta name = "user" instance = google_sql_database_instance.instance.name password = random_password.pwd.result } resource "google_bigquery_connection" "connection" { - provider = google-beta connection_id = "my-connection" location = "US" friendly_name = "👋" @@ -146,17 +136,65 @@ resource "google_bigquery_connection" "connection" { } } ``` + +## Example Usage - Bigquery Connection Cloud Resource + + +```hcl +resource "google_bigquery_connection" "connection" { + connection_id = "my-connection" + location = "US" + friendly_name = "👋" + description = "a riveting description" + cloud_resource {} +} +``` ## Argument Reference The following arguments are supported: + +- - - + + +* `connection_id` - + (Optional) + Optional connection id that should be assigned to the created connection. + +* `location` - + (Optional) + The geographic location where the connection should reside. + Cloud SQL instance must be in the same location as the connection + with following exceptions: Cloud SQL us-central1 maps to BigQuery US, Cloud SQL europe-west1 maps to BigQuery EU. + Examples: US, EU, asia-northeast1, us-central1, europe-west1. The default value is US. + +* `friendly_name` - + (Optional) + A descriptive name for the connection + +* `description` - + (Optional) + A descriptive description for the connection + * `cloud_sql` - - (Required) + (Optional) Cloud SQL properties. Structure is [documented below](#nested_cloud_sql). +* `cloud_resource` - + (Optional) + Cloud Resource properties. + Structure is [documented below](#nested_cloud_resource). + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + The `cloud_sql` block supports: @@ -190,31 +228,10 @@ The following arguments are supported: Password for database. **Note**: This property is sensitive and will not be displayed in the plan. -- - - - - -* `connection_id` - - (Optional) - Optional connection id that should be assigned to the created connection. - -* `location` - - (Optional) - The geographic location where the connection should reside. - Cloud SQL instance must be in the same location as the connection - with following exceptions: Cloud SQL us-central1 maps to BigQuery US, Cloud SQL europe-west1 maps to BigQuery EU. - Examples: US, EU, asia-northeast1, us-central1, europe-west1. The default value is US. - -* `friendly_name` - - (Optional) - A descriptive name for the connection - -* `description` - - (Optional) - A descriptive description for the connection - -* `project` - (Optional) The ID of the project in which the resource belongs. - If it is not provided, the provider project is used. +The `cloud_resource` block supports: +* `service_account_id` - + The account ID of the service created for the purpose of this connection. ## Attributes Reference