diff --git a/mmv1/third_party/terraform/go.mod.erb b/mmv1/third_party/terraform/go.mod.erb index fbcb0911e924..7dde0934c84c 100644 --- a/mmv1/third_party/terraform/go.mod.erb +++ b/mmv1/third_party/terraform/go.mod.erb @@ -4,7 +4,7 @@ module github.com/hashicorp/terraform-provider-google go 1.21 require ( - cloud.google.com/go/bigtable v1.19.0 + cloud.google.com/go/bigtable v1.23.0 github.com/GoogleCloudPlatform/declarative-resource-client-library v1.64.0 github.com/apparentlymart/go-cidr v1.1.0 github.com/davecgh/go-spew v1.1.1 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 fae027d235ad..237477004ae5 100644 --- a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb +++ b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb @@ -289,6 +289,7 @@ var handwrittenResources = map[string]*schema.Resource{ "google_bigtable_gc_policy": bigtable.ResourceBigtableGCPolicy(), "google_bigtable_instance": bigtable.ResourceBigtableInstance(), "google_bigtable_table": bigtable.ResourceBigtableTable(), + "google_bigtable_authorized_view": bigtable.ResourceBigtableAuthorizedView(), "google_billing_subaccount": resourcemanager.ResourceBillingSubaccount(), "google_cloudfunctions_function": cloudfunctions.ResourceCloudFunctionsFunction(), "google_composer_environment": composer.ResourceComposerEnvironment(), diff --git a/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view.go b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view.go new file mode 100644 index 000000000000..a5142e861b3d --- /dev/null +++ b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view.go @@ -0,0 +1,462 @@ +package bigtable + +import ( + "context" + "encoding/base64" + "fmt" + "log" + "reflect" + "time" + + "cloud.google.com/go/bigtable" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +var familySubsetSchemaElem *schema.Resource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "family_name": { + Type: schema.TypeString, + Required: true, + Description: `Name of the column family to be included in the authorized view.`, + }, + "qualifiers": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `Base64-encoded individual exact column qualifiers of the column family to be included in the authorized view.`, + }, + "qualifier_prefixes": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `Base64-encoded prefixes for qualifiers of the column family to be included in the authorized view. Every qualifier starting with one of these prefixes is included in the authorized view. To provide access to all qualifiers, include the empty string as a prefix ("").`, + }, + }, +} + +func ResourceBigtableAuthorizedView() *schema.Resource { + return &schema.Resource{ + Create: resourceBigtableAuthorizedViewCreate, + Read: resourceBigtableAuthorizedViewRead, + Update: resourceBigtableAuthorizedViewUpdate, + Delete: resourceBigtableAuthorizedViewDestroy, + + Importer: &schema.ResourceImporter{ + State: resourceBigtableAuthorizedViewImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + }, + + CustomizeDiff: customdiff.All( + tpgresource.DefaultProviderProject, + ), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The name of the authorized view. Must be 1-50 characters and must only contain hyphens, underscores, periods, letters and numbers.`, + }, + + "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.`, + }, + + "instance_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.CompareResourceNames, + Description: `The name of the Bigtable instance in which the authorized view belongs.`, + }, + + "table_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.CompareResourceNames, + Description: `The name of the Bigtable table in which the authorized view belongs.`, + }, + + "deletion_protection": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"PROTECTED", "UNPROTECTED"}, false), + Description: `A field to make the authorized view protected against data loss i.e. when set to PROTECTED, deleting the authorized view, the table containing the authorized view, and the instance containing the authorized view would be prohibited. If not provided, currently deletion protection will be set to UNPROTECTED as it is the API default value.`, + }, + + "subset_view": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `An AuthorizedView permitting access to an explicit subset of a Table.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "row_prefixes": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `Base64-encoded row prefixes to be included in the authorized view. To provide access to all rows, include the empty string as a prefix ("").`, + }, + "family_subsets": { + Type: schema.TypeSet, + Optional: true, + Description: `Subsets of column families to be included in the authorized view.`, + Elem: familySubsetSchemaElem, + }, + }, + }, + }, + }, + UseJSONNumber: true, + } +} + +func resourceBigtableAuthorizedViewCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + ctx := context.Background() + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return err + } + + instanceName := tpgresource.GetResourceNameFromSelfLink(d.Get("instance_name").(string)) + c, err := config.BigTableClientFactory(userAgent).NewAdminClient(project, instanceName) + if err != nil { + return fmt.Errorf("Error starting admin client. %s", err) + } + if err := d.Set("instance_name", instanceName); err != nil { + return fmt.Errorf("Error setting instance_name: %s", err) + } + + defer c.Close() + + authorizedViewId := d.Get("name").(string) + tableId := d.Get("table_name").(string) + authorizedViewConf := bigtable.AuthorizedViewConf{ + AuthorizedViewID: authorizedViewId, + TableID: tableId, + } + + // Check if deletion protection is given + // If not given, currently tblConf.DeletionProtection will be set to false in the API + deletionProtection := d.Get("deletion_protection") + if deletionProtection == "PROTECTED" { + authorizedViewConf.DeletionProtection = bigtable.Protected + } else if deletionProtection == "UNPROTECTED" { + authorizedViewConf.DeletionProtection = bigtable.Unprotected + } + + subsetView, ok := d.GetOk("subset_view") + if !ok || len(subsetView.([]interface{})) != 1 { + return fmt.Errorf("subset_view must be specified for authorized view %s", authorizedViewId) + } + subsetViewConf, err := generateSubsetViewConfig(subsetView.([]interface{})) + if err != nil { + return err + } + authorizedViewConf.AuthorizedView = subsetViewConf + + ctxWithTimeout, cancel := context.WithTimeout(ctx, d.Timeout(schema.TimeoutCreate)) + defer cancel() // Always call cancel. + err = c.CreateAuthorizedView(ctxWithTimeout, &authorizedViewConf) + if err != nil { + return fmt.Errorf("Error creating authorized view. %s", err) + } + + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/instances/{{instance_name}}/tables/{{table_name}}/authorizedViews/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return resourceBigtableAuthorizedViewRead(d, meta) +} + +func resourceBigtableAuthorizedViewRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + ctx := context.Background() + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return err + } + + instanceName := tpgresource.GetResourceNameFromSelfLink(d.Get("instance_name").(string)) + c, err := config.BigTableClientFactory(userAgent).NewAdminClient(project, instanceName) + if err != nil { + return fmt.Errorf("Error starting admin client. %s", err) + } + + defer c.Close() + + authorizedViewId := d.Get("name").(string) + tableId := d.Get("table_name").(string) + authorizedViewInfo, err := c.AuthorizedViewInfo(ctx, tableId, authorizedViewId) + if err != nil { + if tpgresource.IsNotFoundGrpcError(err) { + log.Printf("[WARN] Removing %s because it's gone", authorizedViewId) + d.SetId("") + return nil + } + return err + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error setting project: %s", err) + } + + deletionProtection := authorizedViewInfo.DeletionProtection + if deletionProtection == bigtable.Protected { + if err := d.Set("deletion_protection", "PROTECTED"); err != nil { + return fmt.Errorf("Error setting deletion_protection: %s", err) + } + } else if deletionProtection == bigtable.Unprotected { + if err := d.Set("deletion_protection", "UNPROTECTED"); err != nil { + return fmt.Errorf("Error setting deletion_protection: %s", err) + } + } else { + return fmt.Errorf("Error setting deletion_protection, it should be either PROTECTED or UNPROTECTED") + } + + if sv, ok := authorizedViewInfo.AuthorizedView.(*bigtable.SubsetViewInfo); ok { + subsetView := flattenSubsetViewInfo(sv) + if err := d.Set("subset_view", subsetView); err != nil { + return fmt.Errorf("Error setting subset_view: %s", err) + } + } else { + return fmt.Errorf("Error parsing server returned subset_view since it's empty") + } + + return nil +} + +func resourceBigtableAuthorizedViewUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + ctx := context.Background() + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return err + } + + instanceName := tpgresource.GetResourceNameFromSelfLink(d.Get("instance_name").(string)) + c, err := config.BigTableClientFactory(userAgent).NewAdminClient(project, instanceName) + if err != nil { + return fmt.Errorf("Error starting admin client. %s", err) + } + defer c.Close() + + authorizedViewId := d.Get("name").(string) + tableId := d.Get("table_name").(string) + authorizedViewConf := bigtable.AuthorizedViewConf{ + AuthorizedViewID: authorizedViewId, + TableID: tableId, + } + + if d.HasChange("subset_view") { + subsetView := d.Get("subset_view") + if len(subsetView.([]interface{})) != 1 { + return fmt.Errorf("subset_view must be specified for authorized view %s", authorizedViewId) + } + subsetViewConf, err := generateSubsetViewConfig(subsetView.([]interface{})) + if err != nil { + return err + } + authorizedViewConf.AuthorizedView = subsetViewConf + } + + if d.HasChange("deletion_protection") { + deletionProtection := d.Get("deletion_protection") + if deletionProtection == "PROTECTED" { + authorizedViewConf.DeletionProtection = bigtable.Protected + } else if deletionProtection == "UNPROTECTED" { + authorizedViewConf.DeletionProtection = bigtable.Unprotected + } + } + + updateAuthorizedViewConf := bigtable.UpdateAuthorizedViewConf{ + AuthorizedViewConf: authorizedViewConf, + IgnoreWarnings: true, + } + + ctxWithTimeout, cancel := context.WithTimeout(ctx, d.Timeout(schema.TimeoutUpdate)) + defer cancel() // Always call cancel. + err = c.UpdateAuthorizedView(ctxWithTimeout, updateAuthorizedViewConf) + if err != nil { + return fmt.Errorf("Error updating authorized view. %s", err) + } + + return resourceBigtableAuthorizedViewRead(d, meta) +} + +func resourceBigtableAuthorizedViewDestroy(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + ctx := context.Background() + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return err + } + + instanceName := tpgresource.GetResourceNameFromSelfLink(d.Get("instance_name").(string)) + c, err := config.BigTableClientFactory(userAgent).NewAdminClient(project, instanceName) + if err != nil { + return fmt.Errorf("Error starting admin client. %s", err) + } + + defer c.Close() + + authorizedViewId := d.Get("name").(string) + tableId := d.Get("table_name").(string) + err = c.DeleteAuthorizedView(ctx, tableId, authorizedViewId) + if err != nil { + return fmt.Errorf("Error deleting authorized view. %s", err) + } + + d.SetId("") + + return nil +} + +func resourceBigtableAuthorizedViewImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*transport_tpg.Config) + if err := tpgresource.ParseImportId([]string{ + "projects/(?P[^/]+)/instances/(?P[^/]+)/tables/(?P[^/]+)/authorizedViews/(?P[^/]+)", + "(?P[^/]+)/(?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}}/instances/{{instance_name}}/tables/{{table_name}}/authorizedViews/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func generateSubsetViewConfig(subsetView []interface{}) (*bigtable.SubsetViewConf, error) { + subsetViewConf := bigtable.SubsetViewConf{} + + if len(subsetView) == 0 { + return nil, fmt.Errorf("Error constructing SubsetViewConfig; empty subset_view list") + } + if subsetView[0] == nil { + return &subsetViewConf, nil + } + sv, ok := subsetView[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Error constructing SubsetViewConfig; element in subset_view list has wrong type: %s", reflect.TypeOf(subsetView[0])) + } + if rowPrefixes, ok := sv["row_prefixes"]; ok { + for _, rowPrefix := range rowPrefixes.(*schema.Set).List() { + decodedRowPrefix, err := base64.StdEncoding.DecodeString(rowPrefix.(string)) + if err != nil { + return nil, err + } + subsetViewConf.AddRowPrefix(decodedRowPrefix) + } + } + if familySubsets, ok := sv["family_subsets"]; ok { + for _, familySubset := range familySubsets.(*schema.Set).List() { + familySubsetElement := familySubset.(map[string]interface{}) + familyName := familySubsetElement["family_name"].(string) + if qualifiers, ok := familySubsetElement["qualifiers"]; ok { + for _, qualifier := range qualifiers.(*schema.Set).List() { + decodedQualifier, err := base64.StdEncoding.DecodeString(qualifier.(string)) + if err != nil { + return nil, err + } + subsetViewConf.AddFamilySubsetQualifier(familyName, decodedQualifier) + } + } + if qualifierPrefixes, ok := familySubsetElement["qualifier_prefixes"]; ok { + for _, qualifierPrefix := range qualifierPrefixes.(*schema.Set).List() { + decodedQualifierPrefix, err := base64.StdEncoding.DecodeString(qualifierPrefix.(string)) + if err != nil { + return nil, err + } + subsetViewConf.AddFamilySubsetQualifierPrefix(familyName, decodedQualifierPrefix) + } + } + } + } + return &subsetViewConf, nil +} + +func flattenSubsetViewInfo(subsetViewInfo *bigtable.SubsetViewInfo) []map[string]interface{} { + subsetView := make([]map[string]interface{}, 1) + + rowPrefixes := []string{} + for _, prefix := range subsetViewInfo.RowPrefixes { + encodedRowPrefix := base64.StdEncoding.EncodeToString(prefix) + rowPrefixes = append(rowPrefixes, encodedRowPrefix) + } + familySubsets := []map[string]interface{}{} + for k, v := range subsetViewInfo.FamilySubsets { + familySubsetElement := make(map[string]interface{}) + familySubsetElement["family_name"] = k + qualifiers := []string{} + for _, qualifier := range v.Qualifiers { + encodedQualifier := base64.StdEncoding.EncodeToString(qualifier) + qualifiers = append(qualifiers, encodedQualifier) + } + if len(qualifiers) > 0 { + familySubsetElement["qualifiers"] = qualifiers + } + qualifierPrefixes := []string{} + for _, qualifierPrefix := range v.QualifierPrefixes { + encodedQualifierPrefix := base64.StdEncoding.EncodeToString(qualifierPrefix) + qualifierPrefixes = append(qualifierPrefixes, encodedQualifierPrefix) + } + if len(qualifierPrefixes) > 0 { + familySubsetElement["qualifier_prefixes"] = qualifierPrefixes + } + familySubsets = append(familySubsets, familySubsetElement) + } + subsetView[0] = make(map[string]interface{}) + if len(rowPrefixes) > 0 { + subsetView[0]["row_prefixes"] = rowPrefixes + } + if len(familySubsets) > 0 { + subsetView[0]["family_subsets"] = familySubsets + } + + return subsetView +} diff --git a/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view_internal_test.go b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view_internal_test.go new file mode 100644 index 000000000000..7b431838426b --- /dev/null +++ b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view_internal_test.go @@ -0,0 +1,346 @@ +package bigtable + +import ( + "reflect" + "strings" + "testing" + + "cloud.google.com/go/bigtable" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestUnitBigtable_flattenSubsetViewInfo(t *testing.T) { + cases := map[string]struct { + sv bigtable.SubsetViewInfo + want []map[string]interface{} + orWant []map[string]interface{} + }{ + "empty subset view": { + sv: bigtable.SubsetViewInfo{}, + want: []map[string]interface{}{ + map[string]interface{}{}, + }, + orWant: nil, + }, + "subset view with row prefixes only": { + sv: bigtable.SubsetViewInfo{ + RowPrefixes: [][]byte{[]byte("row1"), []byte("row2")}, + }, + want: []map[string]interface{}{ + map[string]interface{}{ + "row_prefixes": []string{"cm93MQ==", "cm93Mg=="}, + }, + }, + orWant: nil, + }, + "subset view with family subsets only": { + sv: bigtable.SubsetViewInfo{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam1": { + QualifierPrefixes: [][]byte{[]byte("col")}, + }, + "fam2": { + Qualifiers: [][]byte{[]byte("col1"), []byte("col2")}, + }, + }, + }, + want: []map[string]interface{}{ + map[string]interface{}{ + "family_subsets": []map[string]interface{}{ + map[string]interface{}{ + "family_name": "fam1", + "qualifier_prefixes": []string{"Y29s"}, + }, map[string]interface{}{ + "family_name": "fam2", + "qualifiers": []string{"Y29sMQ==", "Y29sMg=="}, + }, + }, + }, + }, + orWant: []map[string]interface{}{ + map[string]interface{}{ + "family_subsets": []map[string]interface{}{ + map[string]interface{}{ + "family_name": "fam2", + "qualifiers": []string{"Y29sMQ==", "Y29sMg=="}, + }, + map[string]interface{}{ + "family_name": "fam1", + "qualifier_prefixes": []string{"Y29s"}, + }, + }, + }, + }, + }, + "subset view with qualifiers only": { + sv: bigtable.SubsetViewInfo{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam": { + Qualifiers: [][]byte{[]byte("col")}, + }, + }, + }, + want: []map[string]interface{}{ + map[string]interface{}{ + "family_subsets": []map[string]interface{}{ + map[string]interface{}{ + "family_name": "fam", + "qualifiers": []string{"Y29s"}, + }, + }, + }, + }, + orWant: nil, + }, + "subset view with qualifier prefixes only": { + sv: bigtable.SubsetViewInfo{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam": { + QualifierPrefixes: [][]byte{[]byte("col")}, + }, + }, + }, + want: []map[string]interface{}{ + map[string]interface{}{ + "family_subsets": []map[string]interface{}{ + map[string]interface{}{ + "family_name": "fam", + "qualifier_prefixes": []string{"Y29s"}, + }, + }, + }, + }, + orWant: nil, + }, + "subset view with empty arrays": { + sv: bigtable.SubsetViewInfo{ + RowPrefixes: [][]byte{}, + FamilySubsets: map[string]bigtable.FamilySubset{}, + }, + want: []map[string]interface{}{ + map[string]interface{}{}, + }, + orWant: nil, + }, + } + + for tn, tc := range cases { + got := flattenSubsetViewInfo(&tc.sv) + if tc.want != nil && !(reflect.DeepEqual(got, tc.want) || reflect.DeepEqual(got, tc.orWant)) { + t.Errorf("bad: %s, got %q, want %q", tn, got, tc.want) + } + } +} + +func TestUnitBigtable_generateSubsetViewConfig(t *testing.T) { + cases := map[string]struct { + sv []interface{} + want *bigtable.SubsetViewConf + orWant *bigtable.SubsetViewConf + wantError string + }{ + "empty subset view list": { + sv: []interface{}{}, + want: nil, + orWant: nil, + wantError: "empty subset_view list", + }, + "subset view list with wrong type element": { + sv: []interface{}{ + "random-string", + }, + want: nil, + orWant: nil, + wantError: "element in subset_view list has wrong type", + }, + "subset view list with nil element": { + sv: []interface{}{ + nil, + }, + want: &bigtable.SubsetViewConf{}, + orWant: nil, + wantError: "", + }, + "subset view list with empty element": { + sv: []interface{}{ + map[string]interface{}{}, + }, + want: &bigtable.SubsetViewConf{}, + orWant: nil, + wantError: "", + }, + "subset view list with empty lists": { + sv: []interface{}{ + map[string]interface{}{ + "row_prefixes": schema.NewSet(schema.HashString, []interface{}{}), + "family_subsets": schema.NewSet(schema.HashResource(familySubsetSchemaElem), []interface{}{}), + }, + }, + want: &bigtable.SubsetViewConf{}, + orWant: nil, + wantError: "", + }, + "subset view list with row prefixes only": { + sv: []interface{}{ + map[string]interface{}{ + "row_prefixes": schema.NewSet(schema.HashString, []interface{}{"cm93MQ==", "cm93Mg=="}), + }, + }, + want: &bigtable.SubsetViewConf{ + RowPrefixes: [][]byte{[]byte("row1"), []byte("row2")}, + }, + orWant: &bigtable.SubsetViewConf{ + RowPrefixes: [][]byte{[]byte("row2"), []byte("row1")}, + }, + wantError: "", + }, + "subset view list with invalid row prefixes encoding": { + sv: []interface{}{ + map[string]interface{}{ + "row_prefixes": schema.NewSet(schema.HashString, []interface{}{"#"}), + }, + }, + want: nil, + orWant: nil, + wantError: "illegal base64 data", + }, + "subset view list with empty row prefixes element": { + sv: []interface{}{ + map[string]interface{}{ + "row_prefixes": schema.NewSet(schema.HashString, []interface{}{""}), + }, + }, + want: &bigtable.SubsetViewConf{ + RowPrefixes: [][]byte{[]byte("")}, + }, + orWant: nil, + wantError: "", + }, + "subset view list with family subsets only": { + sv: []interface{}{ + map[string]interface{}{ + "family_subsets": schema.NewSet(schema.HashResource(familySubsetSchemaElem), []interface{}{ + map[string]interface{}{ + "family_name": "fam1", + "qualifier_prefixes": schema.NewSet(schema.HashString, []interface{}{"Y29s"}), + }, map[string]interface{}{ + "family_name": "fam2", + "qualifiers": schema.NewSet(schema.HashString, []interface{}{"Y29sMQ==", "Y29sMg=="}), + }, + }), + }, + }, + want: &bigtable.SubsetViewConf{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam1": { + QualifierPrefixes: [][]byte{[]byte("col")}, + }, + "fam2": { + Qualifiers: [][]byte{[]byte("col1"), []byte("col2")}, + }, + }, + }, + orWant: &bigtable.SubsetViewConf{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam1": { + QualifierPrefixes: [][]byte{[]byte("col")}, + }, + "fam2": { + Qualifiers: [][]byte{[]byte("col2"), []byte("col1")}, + }, + }, + }, + wantError: "", + }, + "subset view list with qualifiers only": { + sv: []interface{}{ + map[string]interface{}{ + "family_subsets": schema.NewSet(schema.HashResource(familySubsetSchemaElem), []interface{}{ + map[string]interface{}{ + "family_name": "fam", + "qualifiers": schema.NewSet(schema.HashString, []interface{}{"Y29sMQ==", "Y29sMg=="}), + }, + }), + }, + }, + want: &bigtable.SubsetViewConf{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam": { + Qualifiers: [][]byte{[]byte("col1"), []byte("col2")}, + }, + }, + }, + orWant: &bigtable.SubsetViewConf{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam": { + Qualifiers: [][]byte{[]byte("col2"), []byte("col1")}, + }, + }, + }, + wantError: "", + }, + "subset view list with qualifier prefixes only": { + sv: []interface{}{ + map[string]interface{}{ + "family_subsets": schema.NewSet(schema.HashResource(familySubsetSchemaElem), []interface{}{ + map[string]interface{}{ + "family_name": "fam", + "qualifier_prefixes": schema.NewSet(schema.HashString, []interface{}{"Y29s"}), + }, + }), + }, + }, + want: &bigtable.SubsetViewConf{ + FamilySubsets: map[string]bigtable.FamilySubset{ + "fam": { + QualifierPrefixes: [][]byte{[]byte("col")}, + }, + }, + }, + orWant: nil, + wantError: "", + }, + "subset view list with invalid qualifiers encoding": { + sv: []interface{}{ + map[string]interface{}{ + "family_subsets": schema.NewSet(schema.HashResource(familySubsetSchemaElem), []interface{}{ + map[string]interface{}{ + "family_name": "fam", + "qualifiers": schema.NewSet(schema.HashString, []interface{}{"#"}), + }, + }), + }, + }, + want: nil, + orWant: nil, + wantError: "illegal base64 data", + }, + "subset view list with invalid qualifier prefixes encoding": { + sv: []interface{}{ + map[string]interface{}{ + "family_subsets": schema.NewSet(schema.HashResource(familySubsetSchemaElem), []interface{}{ + map[string]interface{}{ + "family_name": "fam", + "qualifier_prefixes": schema.NewSet(schema.HashString, []interface{}{"#"}), + }, + }), + }, + }, + want: nil, + orWant: nil, + wantError: "illegal base64 data", + }, + } + + for tn, tc := range cases { + got, gotErr := generateSubsetViewConfig(tc.sv) + if (gotErr != nil && tc.wantError == "") || + (gotErr == nil && tc.wantError != "") || + (gotErr != nil && !strings.Contains(gotErr.Error(), tc.wantError)) { + t.Errorf("bad error: %s, got %q, want %q", tn, gotErr, tc.wantError) + } + if tc.want != nil && !(reflect.DeepEqual(got, tc.want) || reflect.DeepEqual(got, tc.orWant)) { + t.Errorf("bad: %s, got %q, want %q", tn, got, tc.want) + } + } +} diff --git a/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view_test.go b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view_test.go new file mode 100644 index 000000000000..15e87531b051 --- /dev/null +++ b/mmv1/third_party/terraform/services/bigtable/resource_bigtable_authorized_view_test.go @@ -0,0 +1,449 @@ +package bigtable_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccBigtableAuthorizedView_basic(t *testing.T) { + // bigtable instance does not use the shared HTTP client, this test creates an instance + acctest.SkipIfVcr(t) + t.Parallel() + + instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + tableName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + authorizedViewName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + familyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigtableTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigtableAuthorizedViewInvalidDeletionProtection(instanceName, tableName, authorizedViewName, familyName), + ExpectError: regexp.MustCompile(".*expected deletion_protection to be one of.*"), + }, + { + Config: testAccBigtableAuthorizedViewInvalidSubsetView(instanceName, tableName, authorizedViewName, familyName), + ExpectError: regexp.MustCompile(".*subset_view must be specified for authorized view.*"), + }, + { + Config: testAccBigtableAuthorizedViewInvalidEncoding(instanceName, tableName, authorizedViewName, familyName), + ExpectError: regexp.MustCompile(".*illegal base64 data.*"), + }, + { + Config: testAccBigtableAuthorizedViewBasic(instanceName, tableName, authorizedViewName, familyName), + }, + { + ResourceName: "google_bigtable_authorized_view.authorized_view", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBigtableAuthorizedViewWithRowPrefixesOnly(instanceName, tableName, authorizedViewName, familyName), + }, + { + ResourceName: "google_bigtable_authorized_view.authorized_view", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccBigtableAuthorizedView_update(t *testing.T) { + // bigtable instance does not use the shared HTTP client, this test creates an instance + acctest.SkipIfVcr(t) + t.Parallel() + + instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + tableName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + authorizedViewName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + familyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigtableTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigtableAuthorizedViewWithQualifiersOnly(instanceName, tableName, authorizedViewName, familyName), + }, + { + ResourceName: "google_bigtable_authorized_view.authorized_view", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBigtableAuthorizedViewWithFamilySubsetsOnly(instanceName, tableName, authorizedViewName, familyName), + }, + { + ResourceName: "google_bigtable_authorized_view.authorized_view", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"subset_view.0.family_subsets"}, // The order of the two family subsets is indeterministic. + }, + }, + }) +} + +func TestAccBigtableAuthorizedView_destroy(t *testing.T) { + // bigtable instance does not use the shared HTTP client, this test creates an instance + acctest.SkipIfVcr(t) + t.Parallel() + + instanceName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + tableName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + authorizedViewName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + familyName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigtableTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigtableAuthorizedViewWithQualifierPrefixesOnly(instanceName, tableName, authorizedViewName, familyName), + }, + { + ResourceName: "google_bigtable_authorized_view.authorized_view", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBigtableAuthorizedViewDestroy(instanceName, tableName, familyName), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckBigtableAuthorizedViewDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + var ctx = context.Background() + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_bigtable_authorized_view" { + continue + } + + config := acctest.GoogleProviderConfig(t) + c, err := config.BigTableClientFactory(config.UserAgent).NewAdminClient(config.Project, rs.Primary.Attributes["instance_name"]) + if err != nil { + // The instance is already gone + return nil + } + + _, err = c.AuthorizedViewInfo(ctx, rs.Primary.Attributes["table_name"], rs.Primary.Attributes["name"]) + if err == nil { + return fmt.Errorf("AuthorizedView still present. Found %s in %s.", rs.Primary.Attributes["name"], rs.Primary.Attributes["table_name"]) + } + + c.Close() + } + + return nil + } +} + +func testAccBigtableAuthorizedViewInvalidDeletionProtection(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "random" + subset_view {} +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName) +} + +func testAccBigtableAuthorizedViewInvalidSubsetView(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName) +} + +func testAccBigtableAuthorizedViewInvalidEncoding(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" + subset_view { + row_prefixes = ["#"] + } +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName) +} + +func testAccBigtableAuthorizedViewBasic(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} + +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" + subset_view {} +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName) +} + +func testAccBigtableAuthorizedViewWithRowPrefixesOnly(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} + +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" + + subset_view { + row_prefixes = [base64encode("row1#"), base64encode("row2#")] + } +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName) +} + +func testAccBigtableAuthorizedViewWithFamilySubsetsOnly(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } + column_family { + family = "%s-second" + } +} + +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" + + subset_view { + family_subsets { + family_name = "%s" + qualifiers = [base64encode("qualifier"), base64encode("qualifier-second")] + } + family_subsets { + family_name = "%s-second" + qualifier_prefixes = [""] + } + } +} +`, instanceName, instanceName, tableName, familyName, familyName, authorizedViewName, familyName, familyName) +} + +func testAccBigtableAuthorizedViewWithQualifiersOnly(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} + +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" + + subset_view { + family_subsets { + family_name = "%s" + qualifiers = [base64encode("qualifier")] + } + } +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName, familyName) +} + +func testAccBigtableAuthorizedViewWithQualifierPrefixesOnly(instanceName, tableName, authorizedViewName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} + +resource "google_bigtable_authorized_view" "authorized_view" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + table_name = google_bigtable_table.table.name + deletion_protection = "UNPROTECTED" + + subset_view { + family_subsets { + family_name = "%s" + qualifier_prefixes = [""] + } + } +} +`, instanceName, instanceName, tableName, familyName, authorizedViewName, familyName) +} + +func testAccBigtableAuthorizedViewDestroy(instanceName, tableName, familyName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + cluster { + cluster_id = "%s" + zone = "us-central1-b" + num_nodes = 1 + } + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.id + column_family { + family = "%s" + } +} +`, instanceName, instanceName, tableName, familyName) +}