From e2a3ee1bd95c873dfcf2cc5c00a0d661beeedcbd Mon Sep 17 00:00:00 2001 From: Megan Bang Date: Wed, 15 Feb 2023 09:49:15 -0600 Subject: [PATCH] Revert "revert plugin framework code (#7287)" This reverts commit 06f9b2ee5fadc6c2537a94d207ae2092b57b3c0b. --- mmv1/provider/terraform/common~compile.yaml | 12 + mmv1/provider/terraform/common~copy.yaml | 6 + .../data_sources/data_source_dns_keys.go | 524 ++++++++---- .../data_source_dns_managed_zone.go | 191 +++-- .../data_source_dns_record_set.go | 169 ++-- .../framework_models/provider_model.go.erb | 74 ++ .../framework_utils/framework_config.go.erb | 744 ++++++++++++++++++ .../framework_provider_test.go | 242 ++++++ .../framework_utils/framework_test_utils.go | 95 +++ .../framework_utils/framework_transport.go | 102 +++ .../framework_utils/framework_utils.go | 139 ++++ .../framework_utils/framework_validators.go | 92 +++ mmv1/third_party/terraform/go.mod.erb | 12 +- mmv1/third_party/terraform/go.sum | 17 + mmv1/third_party/terraform/main.go.erb | 53 +- .../tests/data_source_dns_key_test.go | 44 +- .../data_source_dns_managed_zone_test.go.erb | 83 +- .../tests/data_source_dns_record_set_test.go | 51 -- .../data_source_dns_record_set_test.go.erb | 103 +++ .../third_party/terraform/utils/config.go.erb | 135 ++++ .../terraform/utils/framework_provider.go.erb | 276 +++++++ .../utils/framework_provider_clients.go | 34 + .../terraform/utils/provider.go.erb | 50 +- .../provider_handwritten_endpoint.go.erb | 36 - .../terraform/utils/provider_test.go.erb | 19 +- .../terraform/utils/transport_test.go | 45 ++ mmv1/third_party/terraform/utils/utils.go | 13 + .../templates/provider_dcl_endpoints.go.tmpl | 31 +- 28 files changed, 2953 insertions(+), 439 deletions(-) create mode 100644 mmv1/third_party/terraform/framework_models/provider_model.go.erb create mode 100644 mmv1/third_party/terraform/framework_utils/framework_config.go.erb create mode 100644 mmv1/third_party/terraform/framework_utils/framework_provider_test.go create mode 100644 mmv1/third_party/terraform/framework_utils/framework_test_utils.go create mode 100644 mmv1/third_party/terraform/framework_utils/framework_transport.go create mode 100644 mmv1/third_party/terraform/framework_utils/framework_utils.go create mode 100644 mmv1/third_party/terraform/framework_utils/framework_validators.go delete mode 100644 mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go create mode 100644 mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go.erb create mode 100644 mmv1/third_party/terraform/utils/framework_provider.go.erb create mode 100644 mmv1/third_party/terraform/utils/framework_provider_clients.go diff --git a/mmv1/provider/terraform/common~compile.yaml b/mmv1/provider/terraform/common~compile.yaml index 091203c0ec37..ba98be06c24e 100644 --- a/mmv1/provider/terraform/common~compile.yaml +++ b/mmv1/provider/terraform/common~compile.yaml @@ -38,6 +38,18 @@ -%> '<%= dir -%>/<%= fname.delete_suffix(".erb") -%>': 'third_party/terraform/utils/<%= fname -%>' <% end -%> +<% + Dir["third_party/terraform/framework_utils/*.go.erb"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/<%= fname.delete_suffix(".erb") -%>': 'third_party/terraform/framework_utils/<%= fname -%>' +<% end -%> +<% + Dir["third_party/terraform/framework_models/*.go.erb"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/<%= fname.delete_suffix(".erb") -%>': 'third_party/terraform/framework_models/<%= fname -%>' +<% end -%> <% Dir["third_party/terraform/scripts/**/*.erb"].each do |file_path| fname = file_path.delete_prefix("third_party/terraform/") diff --git a/mmv1/provider/terraform/common~copy.yaml b/mmv1/provider/terraform/common~copy.yaml index 7c36c384d27b..e7252070eb52 100644 --- a/mmv1/provider/terraform/common~copy.yaml +++ b/mmv1/provider/terraform/common~copy.yaml @@ -49,6 +49,12 @@ -%> '<%= fname -%>': '<%= file_path -%>' <% end -%> +<% + Dir["third_party/terraform/framework_utils/*.go"].each do |file_path| + fname = file_path.split('/')[-1] +-%> +'<%= dir -%>/<%= fname -%>': 'third_party/terraform/framework_utils/<%= fname -%>' +<% end -%> '<%= dir -%>/test-fixtures/': 'third_party/terraform/utils/test-fixtures' <% end -%> <% if generate_docs -%> diff --git a/mmv1/third_party/terraform/data_sources/data_source_dns_keys.go b/mmv1/third_party/terraform/data_sources/data_source_dns_keys.go index ea67f13fd2d1..f8a3d7212b06 100644 --- a/mmv1/third_party/terraform/data_sources/data_source_dns_keys.go +++ b/mmv1/third_party/terraform/data_sources/data_source_dns_keys.go @@ -1,229 +1,435 @@ package google import ( + "context" "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "google.golang.org/api/dns/v1" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) -// DNSSEC Algorithm Numbers: https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml -// The following are algorithms that are supported by Cloud DNS -var dnssecAlgoNums = map[string]int{ - "rsasha1": 5, - "rsasha256": 8, - "rsasha512": 10, - "ecdsap256sha256": 13, - "ecdsap384sha384": 14, +var _ datasource.DataSource = &GoogleDnsKeysDataSource{} + +func NewGoogleDnsKeysDataSource() datasource.DataSource { + return &GoogleDnsKeysDataSource{} } -// DS RR Digest Types: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml -// The following are digests that are supported by Cloud DNS -var dnssecDigestType = map[string]int{ - "sha1": 1, - "sha256": 2, - "sha384": 4, +// GoogleDnsKeysDataSource defines the data source implementation +type GoogleDnsKeysDataSource struct { + client *dns.Service + project types.String } -func DataSourceDNSKeys() *schema.Resource { - return &schema.Resource{ - Read: dataSourceDNSKeysRead, +type GoogleDnsKeysModel struct { + Id types.String `tfsdk:"id"` + ManagedZone types.String `tfsdk:"managed_zone"` + Project types.String `tfsdk:"project"` + KeySigningKeys types.List `tfsdk:"key_signing_keys"` + ZoneSigningKeys types.List `tfsdk:"zone_signing_keys"` +} - Schema: map[string]*schema.Schema{ - "managed_zone": { - Type: schema.TypeString, - Required: true, - DiffSuppressFunc: compareSelfLinkOrResourceName, +type GoogleZoneSigningKey struct { + Algorithm types.String `tfsdk:"algorithm"` + CreationTime types.String `tfsdk:"creation_time"` + Description types.String `tfsdk:"description"` + Id types.String `tfsdk:"id"` + IsActive types.Bool `tfsdk:"is_active"` + KeyLength types.Int64 `tfsdk:"key_length"` + KeyTag types.Int64 `tfsdk:"key_tag"` + PublicKey types.String `tfsdk:"public_key"` + Digests types.List `tfsdk:"digests"` +} + +type GoogleKeySigningKey struct { + Algorithm types.String `tfsdk:"algorithm"` + CreationTime types.String `tfsdk:"creation_time"` + Description types.String `tfsdk:"description"` + Id types.String `tfsdk:"id"` + IsActive types.Bool `tfsdk:"is_active"` + KeyLength types.Int64 `tfsdk:"key_length"` + KeyTag types.Int64 `tfsdk:"key_tag"` + PublicKey types.String `tfsdk:"public_key"` + Digests types.List `tfsdk:"digests"` + + DSRecord types.String `tfsdk:"ds_record"` +} + +type GoogleZoneSigningKeyDigest struct { + Digest types.String `tfsdk:"digest"` + Type types.String `tfsdk:"type"` +} + +var ( + digestAttrTypes = map[string]attr.Type{ + "digest": types.StringType, + "type": types.StringType, + } +) + +func (d *GoogleDnsKeysDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_keys" +} + +func (d *GoogleDnsKeysDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Get the DNSKEY and DS records of DNSSEC-signed managed zones", + + Attributes: map[string]schema.Attribute{ + "managed_zone": schema.StringAttribute{ + Description: "The Name of the zone.", + MarkdownDescription: "The Name of the zone.", + Required: true, }, - "project": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, + "project": schema.StringAttribute{ + Description: "The ID of the project for the Google Cloud.", + MarkdownDescription: "The ID of the project for the Google Cloud.", + Optional: true, + Computed: true, }, - "key_signing_keys": { - Type: schema.TypeList, - Computed: true, - Elem: kskResource(), + "id": schema.StringAttribute{ + Description: "DNS keys identifier", + MarkdownDescription: "DNS keys identifier", + Computed: true, }, - "zone_signing_keys": { - Type: schema.TypeList, - Computed: true, - Elem: dnsKeyResource(), + }, + Blocks: map[string]schema.Block{ + "zone_signing_keys": schema.ListNestedBlock{ + Description: "A list of Zone-signing key (ZSK) records.", + MarkdownDescription: "A list of Zone-signing key (ZSK) records.", + NestedObject: dnsKeyObject(), + }, + "key_signing_keys": schema.ListNestedBlock{ + Description: "A list of Key-signing key (KSK) records.", + MarkdownDescription: "A list of Key-signing key (KSK) records.", + NestedObject: kskObject(), }, }, } } -func dnsKeyResource() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "algorithm": { - Type: schema.TypeString, +func (d *GoogleDnsKeysDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + p, ok := req.ProviderData.(*frameworkProvider) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *frameworkProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = p.NewDnsClient(p.userAgent, &resp.Diagnostics) + d.project = p.project +} + +func (d *GoogleDnsKeysDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data GoogleDnsKeysModel + var metaData *ProviderMetaModel + var diags diag.Diagnostics + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + d.client.UserAgent = generateFrameworkUserAgentString(metaData, d.client.UserAgent) + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + fv := parseProjectFieldValueFramework("managedZones", data.ManagedZone.ValueString(), "project", data.Project, d.project, false, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + data.Project = types.StringValue(fv.Project) + data.ManagedZone = types.StringValue(fv.Name) + + data.Id = types.StringValue(fmt.Sprintf("projects/%s/managedZones/%s", data.Project.ValueString(), data.ManagedZone.ValueString())) + + tflog.Debug(ctx, fmt.Sprintf("fetching DNS keys from managed zone %s", data.ManagedZone.ValueString())) + + clientResp, err := d.client.DnsKeys.List(data.Project.ValueString(), data.ManagedZone.ValueString()).Do() + if err != nil && !isGoogleApiErrorWithCode(err, 404) { + diags.AddError("error retrieving DNS keys", err.Error()) + return + } else if isGoogleApiErrorWithCode(err, 404) { + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return + } + + tflog.Trace(ctx, "read dns keys data source") + + zoneSigningKeys, keySigningKeys := flattenSigningKeys(ctx, clientResp.DnsKeys, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + zskObjType := types.ObjectType{}.WithAttributeTypes(getDnsKeyAttrs("zoneSigning")) + data.ZoneSigningKeys, diags = types.ListValueFrom(ctx, zskObjType, zoneSigningKeys) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + kskObjType := types.ObjectType{}.WithAttributeTypes(getDnsKeyAttrs("keySigning")) + data.KeySigningKeys, diags = types.ListValueFrom(ctx, kskObjType, keySigningKeys) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// dnsKeyObject is a helper function for the zone_signing_keys schema and +// is also used by key_signing_keys schema (called in kskObject defined below) +func dnsKeyObject() schema.NestedBlockObject { + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "algorithm": schema.StringAttribute{ + Description: "String mnemonic specifying the DNSSEC algorithm of this key. Immutable after creation time. " + + "Possible values are `ecdsap256sha256`, `ecdsap384sha384`, `rsasha1`, `rsasha256`, and `rsasha512`.", + MarkdownDescription: "String mnemonic specifying the DNSSEC algorithm of this key. Immutable after creation time. " + + "Possible values are `ecdsap256sha256`, `ecdsap384sha384`, `rsasha1`, `rsasha256`, and `rsasha512`.", Computed: true, }, - "creation_time": { - Type: schema.TypeString, - Computed: true, + "creation_time": schema.StringAttribute{ + Description: "The time that this resource was created in the control plane. This is in RFC3339 text format.", + MarkdownDescription: "The time that this resource was created in the control plane. This is in RFC3339 text format.", + Computed: true, }, - "description": { - Type: schema.TypeString, - Computed: true, + "description": schema.StringAttribute{ + Description: "A mutable string of at most 1024 characters associated with this resource for the user's convenience.", + MarkdownDescription: "A mutable string of at most 1024 characters associated with this resource for the user's convenience.", + Computed: true, }, - "digests": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "digest": { - Type: schema.TypeString, - Optional: true, - }, - "type": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, + "id": schema.StringAttribute{ + Description: "Unique identifier for the resource; defined by the server.", + MarkdownDescription: "Unique identifier for the resource; defined by the server.", + Computed: true, }, - "id": { - Type: schema.TypeString, + "is_active": schema.BoolAttribute{ + Description: "Active keys will be used to sign subsequent changes to the ManagedZone. " + + "Inactive keys will still be present as DNSKEY Resource Records for the use of resolvers validating existing signatures.", + MarkdownDescription: "Active keys will be used to sign subsequent changes to the ManagedZone. " + + "Inactive keys will still be present as DNSKEY Resource Records for the use of resolvers validating existing signatures.", Computed: true, }, - "is_active": { - Type: schema.TypeBool, - Computed: true, + "key_length": schema.Int64Attribute{ + Description: "Length of the key in bits. Specified at creation time then immutable.", + MarkdownDescription: "Length of the key in bits. Specified at creation time then immutable.", + Computed: true, }, - "key_length": { - Type: schema.TypeInt, + "key_tag": schema.Int64Attribute{ + Description: "The key tag is a non-cryptographic hash of the a DNSKEY resource record associated with this DnsKey. " + + "The key tag can be used to identify a DNSKEY more quickly (but it is not a unique identifier). " + + "In particular, the key tag is used in a parent zone's DS record to point at the DNSKEY in this child ManagedZone. " + + "The key tag is a number in the range [0, 65535] and the algorithm to calculate it is specified in RFC4034 Appendix B.", + MarkdownDescription: "The key tag is a non-cryptographic hash of the a DNSKEY resource record associated with this DnsKey. " + + "The key tag can be used to identify a DNSKEY more quickly (but it is not a unique identifier). " + + "In particular, the key tag is used in a parent zone's DS record to point at the DNSKEY in this child ManagedZone. " + + "The key tag is a number in the range [0, 65535] and the algorithm to calculate it is specified in RFC4034 Appendix B.", Computed: true, }, - "key_tag": { - Type: schema.TypeInt, - Computed: true, + "public_key": schema.StringAttribute{ + Description: "Base64 encoded public half of this key.", + MarkdownDescription: "Base64 encoded public half of this key.", + Computed: true, }, - "public_key": { - Type: schema.TypeString, - Computed: true, + }, + Blocks: map[string]schema.Block{ + "digests": schema.ListNestedBlock{ + Description: "A list of cryptographic hashes of the DNSKEY resource record associated with this DnsKey. These digests are needed to construct a DS record that points at this DNS key.", + MarkdownDescription: "A list of cryptographic hashes of the DNSKEY resource record associated with this DnsKey. These digests are needed to construct a DS record that points at this DNS key.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "digest": schema.StringAttribute{ + Description: "The base-16 encoded bytes of this digest. Suitable for use in a DS resource record.", + MarkdownDescription: "The base-16 encoded bytes of this digest. Suitable for use in a DS resource record.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Specifies the algorithm used to calculate this digest. Possible values are `sha1`, `sha256` and `sha384`", + MarkdownDescription: "Specifies the algorithm used to calculate this digest. Possible values are `sha1`, `sha256` and `sha384`", + Computed: true, + }, + }, + }, }, }, } } -func kskResource() *schema.Resource { - resource := dnsKeyResource() - - resource.Schema["ds_record"] = &schema.Schema{ - Type: schema.TypeString, - Computed: true, - } - - return resource -} - -func generateDSRecord(signingKey *dns.DnsKey) (string, error) { - algoNum, found := dnssecAlgoNums[signingKey.Algorithm] - if !found { - return "", fmt.Errorf("DNSSEC Algorithm number for %s not found", signingKey.Algorithm) - } +// kskObject is a helper function for the key_signing_keys schema +func kskObject() schema.NestedBlockObject { + nbo := dnsKeyObject() - digestType, found := dnssecDigestType[signingKey.Digests[0].Type] - if !found { - return "", fmt.Errorf("DNSSEC Digest type for %s not found", signingKey.Digests[0].Type) + nbo.Attributes["ds_record"] = schema.StringAttribute{ + Description: "The DS record based on the KSK record.", + MarkdownDescription: "The DS record based on the KSK record.", + Computed: true, } - return fmt.Sprintf("%d %d %d %s", - signingKey.KeyTag, - algoNum, - digestType, - signingKey.Digests[0].Digest), nil + return nbo } -func flattenSigningKeys(signingKeys []*dns.DnsKey, keyType string) []map[string]interface{} { - var keys []map[string]interface{} +func flattenSigningKeys(ctx context.Context, signingKeys []*dns.DnsKey, diags *diag.Diagnostics) ([]types.Object, []types.Object) { + var zoneSigningKeys []types.Object + var keySigningKeys []types.Object + var d diag.Diagnostics for _, signingKey := range signingKeys { - if signingKey != nil && signingKey.Type == keyType { - data := map[string]interface{}{ - "algorithm": signingKey.Algorithm, - "creation_time": signingKey.CreationTime, - "description": signingKey.Description, - "digests": flattenDigests(signingKey.Digests), - "id": signingKey.Id, - "is_active": signingKey.IsActive, - "key_length": signingKey.KeyLength, - "key_tag": signingKey.KeyTag, - "public_key": signingKey.PublicKey, + if signingKey != nil { + var digests []types.Object + for _, dig := range signingKey.Digests { + digest := GoogleZoneSigningKeyDigest{ + Digest: types.StringValue(dig.Digest), + Type: types.StringValue(dig.Type), + } + obj, d := types.ObjectValueFrom(ctx, digestAttrTypes, digest) + diags.Append(d...) + if diags.HasError() { + return zoneSigningKeys, keySigningKeys + } + + digests = append(digests, obj) } if signingKey.Type == "keySigning" && len(signingKey.Digests) > 0 { + ksk := GoogleKeySigningKey{ + Algorithm: types.StringValue(signingKey.Algorithm), + CreationTime: types.StringValue(signingKey.CreationTime), + Description: types.StringValue(signingKey.Description), + Id: types.StringValue(signingKey.Id), + IsActive: types.BoolValue(signingKey.IsActive), + KeyLength: types.Int64Value(signingKey.KeyLength), + KeyTag: types.Int64Value(signingKey.KeyTag), + PublicKey: types.StringValue(signingKey.PublicKey), + } + + objType := types.ObjectType{}.WithAttributeTypes(digestAttrTypes) + ksk.Digests, d = types.ListValueFrom(ctx, objType, digests) + diags.Append(d...) + if diags.HasError() { + return zoneSigningKeys, keySigningKeys + } + dsRecord, err := generateDSRecord(signingKey) - if err == nil { - data["ds_record"] = dsRecord + if err != nil { + diags.AddError("error generating ds record", err.Error()) + return zoneSigningKeys, keySigningKeys } - } - keys = append(keys, data) - } - } + ksk.DSRecord = types.StringValue(dsRecord) - return keys -} + obj, d := types.ObjectValueFrom(ctx, getDnsKeyAttrs(signingKey.Type), ksk) + diags.Append(d...) + if diags.HasError() { + return zoneSigningKeys, keySigningKeys + } + keySigningKeys = append(keySigningKeys, obj) + } else { + zsk := GoogleZoneSigningKey{ + Algorithm: types.StringValue(signingKey.Algorithm), + CreationTime: types.StringValue(signingKey.CreationTime), + Description: types.StringValue(signingKey.Description), + Id: types.StringValue(signingKey.Id), + IsActive: types.BoolValue(signingKey.IsActive), + KeyLength: types.Int64Value(signingKey.KeyLength), + KeyTag: types.Int64Value(signingKey.KeyTag), + PublicKey: types.StringValue(signingKey.PublicKey), + } -func flattenDigests(dnsKeyDigests []*dns.DnsKeyDigest) []map[string]interface{} { - var digests []map[string]interface{} + objType := types.ObjectType{}.WithAttributeTypes(digestAttrTypes) + zsk.Digests, d = types.ListValueFrom(ctx, objType, digests) + diags.Append(d...) + if diags.HasError() { + return zoneSigningKeys, keySigningKeys + } - for _, dnsKeyDigest := range dnsKeyDigests { - if dnsKeyDigest != nil { - data := map[string]interface{}{ - "digest": dnsKeyDigest.Digest, - "type": dnsKeyDigest.Type, + obj, d := types.ObjectValueFrom(ctx, getDnsKeyAttrs("zoneSigning"), zsk) + diags.Append(d...) + if diags.HasError() { + return zoneSigningKeys, keySigningKeys + } + zoneSigningKeys = append(zoneSigningKeys, obj) } - digests = append(digests, data) } } - return digests + return zoneSigningKeys, keySigningKeys } -func dataSourceDNSKeysRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - userAgent, err := generateUserAgentString(d, config.UserAgent) - if err != nil { - return err - } +// DNSSEC Algorithm Numbers: https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml +// The following are algorithms that are supported by Cloud DNS +var dnssecAlgoNums = map[string]int{ + "rsasha1": 5, + "rsasha256": 8, + "rsasha512": 10, + "ecdsap256sha256": 13, + "ecdsap384sha384": 14, +} - fv, err := parseProjectFieldValue("managedZones", d.Get("managed_zone").(string), "project", d, config, false) - if err != nil { - return err - } - project := fv.Project - managedZone := fv.Name +// DS RR Digest Types: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml +// The following are digests that are supported by Cloud DNS +var dnssecDigestType = map[string]int{ + "sha1": 1, + "sha256": 2, + "sha384": 4, +} - if err := d.Set("project", project); err != nil { - return fmt.Errorf("Error setting project: %s", err) +// generateDSRecord will generate the ds_record on key signing keys +func generateDSRecord(signingKey *dns.DnsKey) (string, error) { + algoNum, found := dnssecAlgoNums[signingKey.Algorithm] + if !found { + return "", fmt.Errorf("DNSSEC Algorithm number for %s not found", signingKey.Algorithm) } - d.SetId(fmt.Sprintf("projects/%s/managedZones/%s", project, managedZone)) - log.Printf("[DEBUG] Fetching DNS keys from managed zone %s", managedZone) - - response, err := config.NewDnsClient(userAgent).DnsKeys.List(project, managedZone).Do() - if err != nil && !IsGoogleApiErrorWithCode(err, 404) { - return fmt.Errorf("error retrieving DNS keys: %s", err) - } else if IsGoogleApiErrorWithCode(err, 404) { - return nil + digestType, found := dnssecDigestType[signingKey.Digests[0].Type] + if !found { + return "", fmt.Errorf("DNSSEC Digest type for %s not found", signingKey.Digests[0].Type) } - log.Printf("[DEBUG] Fetched DNS keys from managed zone %s", managedZone) + return fmt.Sprintf("%d %d %d %s", + signingKey.KeyTag, + algoNum, + digestType, + signingKey.Digests[0].Digest), nil +} - if err := d.Set("key_signing_keys", flattenSigningKeys(response.DnsKeys, "keySigning")); err != nil { - return fmt.Errorf("Error setting key_signing_keys: %s", err) +func getDnsKeyAttrs(keyType string) map[string]attr.Type { + dnsKeyAttrs := map[string]attr.Type{ + "algorithm": types.StringType, + "creation_time": types.StringType, + "description": types.StringType, + "id": types.StringType, + "is_active": types.BoolType, + "key_length": types.Int64Type, + "key_tag": types.Int64Type, + "public_key": types.StringType, + "digests": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(digestAttrTypes)), } - if err := d.Set("zone_signing_keys", flattenSigningKeys(response.DnsKeys, "zoneSigning")); err != nil { - return fmt.Errorf("Error setting zone_signing_keys: %s", err) + + if keyType == "keySigning" { + dnsKeyAttrs["ds_record"] = types.StringType } - return nil + return dnsKeyAttrs } diff --git a/mmv1/third_party/terraform/data_sources/data_source_dns_managed_zone.go b/mmv1/third_party/terraform/data_sources/data_source_dns_managed_zone.go index dd41b79b304f..6d45298b4b8a 100644 --- a/mmv1/third_party/terraform/data_sources/data_source_dns_managed_zone.go +++ b/mmv1/third_party/terraform/data_sources/data_source_dns_managed_zone.go @@ -1,101 +1,170 @@ package google import ( + "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/dns/v1" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) -func DataSourceDnsManagedZone() *schema.Resource { - return &schema.Resource{ - Read: dataSourceDnsManagedZoneRead, +var _ datasource.DataSource = &GoogleDnsManagedZoneDataSource{} - Schema: map[string]*schema.Schema{ - "dns_name": { - Type: schema.TypeString, - Computed: true, +func NewGoogleDnsManagedZoneDataSource() datasource.DataSource { + return &GoogleDnsManagedZoneDataSource{} +} + +// GoogleDnsManagedZoneDataSource defines the data source implementation +type GoogleDnsManagedZoneDataSource struct { + client *dns.Service + project types.String +} + +type GoogleDnsManagedZoneModel struct { + Id types.String `tfsdk:"id"` + DnsName types.String `tfsdk:"dns_name"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + ManagedZoneId types.Int64 `tfsdk:"managed_zone_id"` + NameServers types.List `tfsdk:"name_servers"` + Visibility types.String `tfsdk:"visibility"` + Project types.String `tfsdk:"project"` +} + +func (d *GoogleDnsManagedZoneDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_managed_zone" +} + +func (d *GoogleDnsManagedZoneDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Provides access to a zone's attributes within Google Cloud DNS", + + Attributes: map[string]schema.Attribute{ + "dns_name": schema.StringAttribute{ + Description: "The fully qualified DNS name of this zone.", + MarkdownDescription: "The fully qualified DNS name of this zone.", + Computed: true, }, - "name": { - Type: schema.TypeString, - Required: true, + "name": schema.StringAttribute{ + Description: "A unique name for the resource.", + MarkdownDescription: "A unique name for the resource.", + Required: true, }, - "description": { - Type: schema.TypeString, - Computed: true, + "description": schema.StringAttribute{ + Description: "A textual description field.", + MarkdownDescription: "A textual description field.", + Computed: true, }, - "managed_zone_id": { - Type: schema.TypeInt, - Computed: true, - Description: `Unique identifier for the resource; defined by the server.`, + "managed_zone_id": schema.Int64Attribute{ + Description: "Unique identifier for the resource; defined by the server.", + MarkdownDescription: "Unique identifier for the resource; defined by the server.", + Computed: true, }, - "name_servers": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + "name_servers": schema.ListAttribute{ + Description: "The list of nameservers that will be authoritative for this " + + "domain. Use NS records to redirect from your DNS provider to these names, " + + "thus making Google Cloud DNS authoritative for this zone.", + MarkdownDescription: "The list of nameservers that will be authoritative for this " + + "domain. Use NS records to redirect from your DNS provider to these names, " + + "thus making Google Cloud DNS authoritative for this zone.", + Computed: true, + ElementType: types.StringType, }, - "visibility": { - Type: schema.TypeString, + "visibility": schema.StringAttribute{ + Description: "The zone's visibility: public zones are exposed to the Internet, " + + "while private zones are visible only to Virtual Private Cloud resources.", + MarkdownDescription: "The zone's visibility: public zones are exposed to the Internet, " + + "while private zones are visible only to Virtual Private Cloud resources.", Computed: true, }, // Google Cloud DNS ManagedZone resources do not have a SelfLink attribute. - "project": { - Type: schema.TypeString, - Optional: true, + "project": schema.StringAttribute{ + Description: "The ID of the project for the Google Cloud.", + MarkdownDescription: "The ID of the project for the Google Cloud.", + Optional: true, + }, + "id": schema.StringAttribute{ + Description: "DNS managed zone identifier", + MarkdownDescription: "DNS managed zone identifier", + Computed: true, }, }, } } -func dataSourceDnsManagedZoneRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - userAgent, err := generateUserAgentString(d, config.UserAgent) - if err != nil { - return err +func (d *GoogleDnsManagedZoneDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return } - project, err := getProject(d, config) - if err != nil { - return err + p, ok := req.ProviderData.(*frameworkProvider) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *frameworkProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return } - name := d.Get("name").(string) - d.SetId(fmt.Sprintf("projects/%s/managedZones/%s", project, name)) + d.client = p.NewDnsClient(p.userAgent, &resp.Diagnostics) + d.project = p.project +} - zone, err := config.NewDnsClient(userAgent).ManagedZones.Get( - project, name).Do() - if err != nil { - return handleNotFoundError(err, d, fmt.Sprintf("DataSourceDnsManagedZone %q", name)) - } +func (d *GoogleDnsManagedZoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data GoogleDnsManagedZoneModel + var metaData *ProviderMetaModel + var diags diag.Diagnostics - if err := d.Set("dns_name", zone.DnsName); err != nil { - return fmt.Errorf("Error setting dns_name: %s", err) - } - if err := d.Set("name", zone.Name); err != nil { - return fmt.Errorf("Error setting name: %s", err) - } - if err := d.Set("description", zone.Description); err != nil { - return fmt.Errorf("Error setting description: %s", err) + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return } - if err := d.Set("managed_zone_id", zone.Id); err != nil { - return fmt.Errorf("Error setting managed_zone_id: %s", err) + + d.client.UserAgent = generateFrameworkUserAgentString(metaData, d.client.UserAgent) + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - if err := d.Set("name_servers", zone.NameServers); err != nil { - return fmt.Errorf("Error setting name_servers: %s", err) + + data.Project = getProjectFramework(data.Project, d.project, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return } - if err := d.Set("visibility", zone.Visibility); err != nil { - return fmt.Errorf("Error setting visibility: %s", err) + + data.Id = types.StringValue(fmt.Sprintf("projects/%s/managedZones/%s", data.Project.ValueString(), data.Name.ValueString())) + clientResp, err := d.client.ManagedZones.Get(data.Project.ValueString(), data.Name.ValueString()).Do() + if err != nil { + handleDatasourceNotFoundError(ctx, err, &resp.State, fmt.Sprintf("dataSourceDnsManagedZone %q", data.Name.ValueString()), &resp.Diagnostics) } - if err := d.Set("project", project); err != nil { - return fmt.Errorf("Error setting project: %s", err) + + tflog.Trace(ctx, "read dns record set data source") + + data.DnsName = types.StringValue(clientResp.DnsName) + data.Description = types.StringValue(clientResp.Description) + data.ManagedZoneId = types.Int64Value(int64(clientResp.Id)) + data.Visibility = types.StringValue(clientResp.Visibility) + data.NameServers, diags = types.ListValueFrom(ctx, types.StringType, clientResp.NameServers) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return nil + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/mmv1/third_party/terraform/data_sources/data_source_dns_record_set.go b/mmv1/third_party/terraform/data_sources/data_source_dns_record_set.go index 53ee1f4079a0..5743f80068f3 100644 --- a/mmv1/third_party/terraform/data_sources/data_source_dns_record_set.go +++ b/mmv1/third_party/terraform/data_sources/data_source_dns_record_set.go @@ -1,86 +1,145 @@ package google import ( + "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/api/dns/v1" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) -func DataSourceDnsRecordSet() *schema.Resource { - return &schema.Resource{ - Read: dataSourceDnsRecordSetRead, +var _ datasource.DataSource = &GoogleDnsRecordSetDataSource{} - Schema: map[string]*schema.Schema{ - "managed_zone": { - Type: schema.TypeString, - Required: true, - }, +func NewGoogleDnsRecordSetDataSource() datasource.DataSource { + return &GoogleDnsRecordSetDataSource{} +} - "name": { - Type: schema.TypeString, - Required: true, - }, +// GoogleDnsRecordSetDataSource defines the data source implementation +type GoogleDnsRecordSetDataSource struct { + client *dns.Service + project types.String +} - "rrdatas": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, +type GoogleDnsRecordSetModel struct { + Id types.String `tfsdk:"id"` + ManagedZone types.String `tfsdk:"managed_zone"` + Name types.String `tfsdk:"name"` + Rrdatas types.List `tfsdk:"rrdatas"` + Ttl types.Int64 `tfsdk:"ttl"` + Type types.String `tfsdk:"type"` + Project types.String `tfsdk:"project"` +} - "ttl": { - Type: schema.TypeInt, - Computed: true, - }, +func (d *GoogleDnsRecordSetDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dns_record_set" +} - "type": { - Type: schema.TypeString, - Required: true, - }, +func (d *GoogleDnsRecordSetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "A DNS record set within Google Cloud DNS", - "project": { - Type: schema.TypeString, - Optional: true, + Attributes: map[string]schema.Attribute{ + "managed_zone": schema.StringAttribute{ + MarkdownDescription: "The Name of the zone.", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The DNS name for the resource.", + Required: true, + }, + "rrdatas": schema.ListAttribute{ + MarkdownDescription: "The string data for the records in this record set.", + Computed: true, + ElementType: types.StringType, + }, + "ttl": schema.Int64Attribute{ + MarkdownDescription: "The time-to-live of this record set (seconds).", + Computed: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The identifier of a supported record type. See the list of Supported DNS record types.", + Required: true, + }, + "project": schema.StringAttribute{ + MarkdownDescription: "The ID of the project for the Google Cloud.", + Optional: true, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "DNS record set identifier", + Computed: true, }, }, } } -func dataSourceDnsRecordSetRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - userAgent, err := generateUserAgentString(d, config.UserAgent) - if err != nil { - return err +func (d *GoogleDnsRecordSetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return } - project, err := getProject(d, config) - if err != nil { - return err + p, ok := req.ProviderData.(*frameworkProvider) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *frameworkProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return } - zone := d.Get("managed_zone").(string) - name := d.Get("name").(string) - dnsType := d.Get("type").(string) - d.SetId(fmt.Sprintf("projects/%s/managedZones/%s/rrsets/%s/%s", project, zone, name, dnsType)) + d.client = p.NewDnsClient(p.userAgent, &resp.Diagnostics) + d.project = p.project +} - resp, err := config.NewDnsClient(userAgent).ResourceRecordSets.List(project, zone).Name(name).Type(dnsType).Do() - if err != nil { - return handleNotFoundError(err, d, fmt.Sprintf("DataSourceDnsRecordSet %q", name)) +func (d *GoogleDnsRecordSetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data GoogleDnsRecordSetModel + var metaData *ProviderMetaModel + var diags diag.Diagnostics + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return } - if len(resp.Rrsets) != 1 { - return fmt.Errorf("Only expected 1 record set, got %d", len(resp.Rrsets)) + + d.client.UserAgent = generateFrameworkUserAgentString(metaData, d.client.UserAgent) + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - if err := d.Set("rrdatas", resp.Rrsets[0].Rrdatas); err != nil { - return fmt.Errorf("Error setting rrdatas: %s", err) + data.Project = getProjectFramework(data.Project, d.project, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return } - if err := d.Set("ttl", resp.Rrsets[0].Ttl); err != nil { - return fmt.Errorf("Error setting ttl: %s", err) + + data.Id = types.StringValue(fmt.Sprintf("projects/%s/managedZones/%s/rrsets/%s/%s", data.Project.ValueString(), data.ManagedZone.ValueString(), data.Name.ValueString(), data.Type.ValueString())) + clientResp, err := d.client.ResourceRecordSets.List(data.Project.ValueString(), data.ManagedZone.ValueString()).Name(data.Name.ValueString()).Type(data.Type.ValueString()).Do() + if err != nil { + handleDatasourceNotFoundError(ctx, err, &resp.State, fmt.Sprintf("dataSourceDnsRecordSet %q", data.Name.ValueString()), &resp.Diagnostics) } - if err := d.Set("project", project); err != nil { - return fmt.Errorf("Error setting project: %s", err) + if len(clientResp.Rrsets) != 1 { + resp.Diagnostics.AddError("only expected 1 record set", fmt.Sprintf("%d record sets were returned", len(clientResp.Rrsets))) + } + + tflog.Trace(ctx, "read dns record set data source") + + data.Type = types.StringValue(clientResp.Rrsets[0].Type) + data.Ttl = types.Int64Value(clientResp.Rrsets[0].Ttl) + data.Rrdatas, diags = types.ListValueFrom(ctx, types.StringType, clientResp.Rrsets[0].Rrdatas) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return nil + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/mmv1/third_party/terraform/framework_models/provider_model.go.erb b/mmv1/third_party/terraform/framework_models/provider_model.go.erb new file mode 100644 index 000000000000..00038b86374c --- /dev/null +++ b/mmv1/third_party/terraform/framework_models/provider_model.go.erb @@ -0,0 +1,74 @@ +<% autogen_exception -%> +package google + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// ProviderModel describes the provider data model. +type ProviderModel struct { + Credentials types.String `tfsdk:"credentials"` + AccessToken types.String `tfsdk:"access_token"` + ImpersonateServiceAccount types.String `tfsdk:"impersonate_service_account"` + ImpersonateServiceAccountDelegates types.List `tfsdk:"impersonate_service_account_delegates"` + Project types.String `tfsdk:"project"` + BillingProject types.String `tfsdk:"billing_project"` + Region types.String `tfsdk:"region"` + Zone types.String `tfsdk:"zone"` + Scopes types.List `tfsdk:"scopes"` + Batching types.List `tfsdk:"batching"` + UserProjectOverride types.Bool `tfsdk:"user_project_override"` + RequestTimeout types.String `tfsdk:"request_timeout"` + RequestReason types.String `tfsdk:"request_reason"` + + // Generated Products +<% products.each do |product| -%> + <%= product[:definitions].name -%>CustomEndpoint types.String `tfsdk:"<%= product[:definitions].name.underscore -%>_custom_endpoint"` +<% end -%> + + // Handwritten Products / Versioned / Atypical Entries + CloudBillingCustomEndpoint types.String `tfsdk:"cloud_billing_custom_endpoint"` + ComposerCustomEndpoint types.String `tfsdk:"composer_custom_endpoint"` + ContainerCustomEndpoint types.String `tfsdk:"container_custom_endpoint"` + DataflowCustomEndpoint types.String `tfsdk:"dataflow_custom_endpoint"` + IamCredentialsCustomEndpoint types.String `tfsdk:"iam_credentials_custom_endpoint"` + ResourceManagerV3CustomEndpoint types.String `tfsdk:"resource_manager_v3_custom_endpoint"` +<% unless version == "ga" -%> + RuntimeconfigCustomEndpoint types.String `tfsdk:"runtimeconfig_custom_endpoint"` +<% end -%> + IAMCustomEndpoint types.String `tfsdk:"iam_custom_endpoint"` + ServiceNetworkingCustomEndpoint types.String `tfsdk:"service_networking_custom_endpoint"` + TagsLocationCustomEndpoint types.String `tfsdk:"tags_location_custom_endpoint"` + + // dcl + ContainerAwsCustomEndpoint types.String `tfsdk:"container_aws_custom_endpoint"` + ContainerAzureCustomEndpoint types.String `tfsdk:"container_azure_custom_endpoint"` + + // dcl generated + ApikeysCustomEndpoint types.String `tfsdk:"apikeys_custom_endpoint"` + AssuredWorkloadsCustomEndpoint types.String `tfsdk:"assured_workloads_custom_endpoint"` + CloudBuildWorkerPoolCustomEndpoint types.String `tfsdk:"cloud_build_worker_pool_custom_endpoint"` + CloudDeployCustomEndpoint types.String `tfsdk:"clouddeploy_custom_endpoint"` + CloudResourceManagerCustomEndpoint types.String `tfsdk:"cloud_resource_manager_custom_endpoint"` + DataplexCustomEndpoint types.String `tfsdk:"dataplex_custom_endpoint"` + EventarcCustomEndpoint types.String `tfsdk:"eventarc_custom_endpoint"` + FirebaserulesCustomEndpoint types.String `tfsdk:"firebaserules_custom_endpoint"` + NetworkConnectivityCustomEndpoint types.String `tfsdk:"network_connectivity_custom_endpoint"` +<% if version == "ga" -%> + OrgPolicyCustomEndpoint types.String `tfsdk:"org_policy_custom_endpoint"` +<% end -%> + RecaptchaEnterpriseCustomEndpoint types.String `tfsdk:"recaptcha_enterprise_custom_endpoint"` +<% unless version == "ga" -%> + GkehubFeatureCustomEndpoint types.String `tfsdk:"gkehub_feature_custom_endpoint"` +<% end -%> +} + +type ProviderBatching struct { + SendAfter types.String `tfsdk:"send_after"` + EnableBatching types.Bool `tfsdk:"enable_batching"` +} + +// ProviderMetaModel describes the provider meta model +type ProviderMetaModel struct { + ModuleName types.String `tfsdk:"module_name"` +} diff --git a/mmv1/third_party/terraform/framework_utils/framework_config.go.erb b/mmv1/third_party/terraform/framework_utils/framework_config.go.erb new file mode 100644 index 000000000000..42b73fec6652 --- /dev/null +++ b/mmv1/third_party/terraform/framework_utils/framework_config.go.erb @@ -0,0 +1,744 @@ +<% autogen_exception -%> +package google + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + "golang.org/x/oauth2" + googleoauth "golang.org/x/oauth2/google" + + "google.golang.org/api/option" + "google.golang.org/api/transport" + "google.golang.org/grpc" + + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" + + grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" + "github.com/sirupsen/logrus" +) + +// Provider methods + +// ConfigureWithData handles the bulk of configuring the provider +// it is pulled out so that we can manually call this from our testing provider as well +func (p *frameworkProvider) ConfigureWithData(ctx context.Context, data ProviderModel, tfVersion string, diags *diag.Diagnostics) { + // Set defaults if needed + p.HandleDefaults(ctx, &data, diags) + if diags.HasError() { + return + } + + // Handle User Agent string + p.userAgent = CompileUserAgentString(ctx, "terraform-provider-google<%= "-" + version unless version == 'ga' -%>", tfVersion, p.version) + // opt in extension for adding to the User-Agent header + if ext := os.Getenv("GOOGLE_TERRAFORM_USERAGENT_EXTENSION"); ext != "" { + ua := p.userAgent + p.userAgent = fmt.Sprintf("%s %s", ua, ext) + } + + // Set up client configuration + p.SetupClient(ctx, data, diags) + if diags.HasError() { + return + } + + // gRPC Logging setup + p.SetupGrpcLogging() + + // Handle Batching Config + batchingConfig := GetBatchingConfig(ctx, data.Batching, diags) + if diags.HasError() { + return + } + + // Setup Base Paths for clients + // Generated products + p.AccessApprovalBasePath = data.AccessApprovalCustomEndpoint.ValueString() + p.AccessContextManagerBasePath = data.AccessContextManagerCustomEndpoint.ValueString() + p.ActiveDirectoryBasePath = data.ActiveDirectoryCustomEndpoint.ValueString() + p.ApigeeBasePath = data.ApigeeCustomEndpoint.ValueString() + p.AppEngineBasePath = data.AppEngineCustomEndpoint.ValueString() + p.ArtifactRegistryBasePath = data.ArtifactRegistryCustomEndpoint.ValueString() + p.BeyondcorpBasePath = data.BeyondcorpCustomEndpoint.ValueString() + p.BigQueryBasePath = data.BigQueryCustomEndpoint.ValueString() + p.BigqueryAnalyticsHubBasePath = data.BigqueryAnalyticsHubCustomEndpoint.ValueString() + p.BigqueryConnectionBasePath = data.BigqueryConnectionCustomEndpoint.ValueString() + p.BigqueryDataTransferBasePath = data.BigqueryDataTransferCustomEndpoint.ValueString() + p.BigqueryReservationBasePath = data.BigqueryReservationCustomEndpoint.ValueString() + p.BigtableBasePath = data.BigtableCustomEndpoint.ValueString() + p.BillingBasePath = data.BillingCustomEndpoint.ValueString() + p.BinaryAuthorizationBasePath = data.BinaryAuthorizationCustomEndpoint.ValueString() + p.CertificateManagerBasePath = data.CertificateManagerCustomEndpoint.ValueString() + p.CloudAssetBasePath = data.CloudAssetCustomEndpoint.ValueString() + p.CloudBuildBasePath = data.CloudBuildCustomEndpoint.ValueString() + p.CloudFunctionsBasePath = data.CloudFunctionsCustomEndpoint.ValueString() + p.Cloudfunctions2BasePath = data.Cloudfunctions2CustomEndpoint.ValueString() + p.CloudIdentityBasePath = data.CloudIdentityCustomEndpoint.ValueString() + p.CloudIdsBasePath = data.CloudIdsCustomEndpoint.ValueString() + p.CloudIotBasePath = data.CloudIotCustomEndpoint.ValueString() + p.CloudRunBasePath = data.CloudRunCustomEndpoint.ValueString() + p.CloudRunV2BasePath = data.CloudRunV2CustomEndpoint.ValueString() + p.CloudSchedulerBasePath = data.CloudSchedulerCustomEndpoint.ValueString() + p.CloudTasksBasePath = data.CloudTasksCustomEndpoint.ValueString() + p.ComputeBasePath = data.ComputeCustomEndpoint.ValueString() + p.ContainerAnalysisBasePath = data.ContainerAnalysisCustomEndpoint.ValueString() + p.ContainerAttachedBasePath = data.ContainerAttachedCustomEndpoint.ValueString() + p.DataCatalogBasePath = data.DataCatalogCustomEndpoint.ValueString() + p.DataFusionBasePath = data.DataFusionCustomEndpoint.ValueString() + p.DataLossPreventionBasePath = data.DataLossPreventionCustomEndpoint.ValueString() + p.DataprocBasePath = data.DataprocCustomEndpoint.ValueString() + p.DataprocMetastoreBasePath = data.DataprocMetastoreCustomEndpoint.ValueString() + p.DatastoreBasePath = data.DatastoreCustomEndpoint.ValueString() + p.DatastreamBasePath = data.DatastreamCustomEndpoint.ValueString() + p.DeploymentManagerBasePath = data.DeploymentManagerCustomEndpoint.ValueString() + p.DialogflowBasePath = data.DialogflowCustomEndpoint.ValueString() + p.DialogflowCXBasePath = data.DialogflowCXCustomEndpoint.ValueString() + p.DNSBasePath = data.DNSCustomEndpoint.ValueString() + p.DocumentAIBasePath = data.DocumentAICustomEndpoint.ValueString() + p.EssentialContactsBasePath = data.EssentialContactsCustomEndpoint.ValueString() + p.FilestoreBasePath = data.FilestoreCustomEndpoint.ValueString() + p.FirestoreBasePath = data.FirestoreCustomEndpoint.ValueString() + p.GameServicesBasePath = data.GameServicesCustomEndpoint.ValueString() + p.GKEBackupBasePath = data.GKEBackupCustomEndpoint.ValueString() + p.GKEHubBasePath = data.GKEHubCustomEndpoint.ValueString() + p.HealthcareBasePath = data.HealthcareCustomEndpoint.ValueString() + p.IAM2BasePath = data.IAM2CustomEndpoint.ValueString() + p.IAMBetaBasePath = data.IAMBetaCustomEndpoint.ValueString() + p.IAMWorkforcePoolBasePath = data.IAMWorkforcePoolCustomEndpoint.ValueString() + p.IapBasePath = data.IapCustomEndpoint.ValueString() + p.IdentityPlatformBasePath = data.IdentityPlatformCustomEndpoint.ValueString() + p.KMSBasePath = data.KMSCustomEndpoint.ValueString() + p.LoggingBasePath = data.LoggingCustomEndpoint.ValueString() + p.MemcacheBasePath = data.MemcacheCustomEndpoint.ValueString() + p.MLEngineBasePath = data.MLEngineCustomEndpoint.ValueString() + p.MonitoringBasePath = data.MonitoringCustomEndpoint.ValueString() + p.NetworkManagementBasePath = data.NetworkManagementCustomEndpoint.ValueString() + p.NetworkServicesBasePath = data.NetworkServicesCustomEndpoint.ValueString() + p.NotebooksBasePath = data.NotebooksCustomEndpoint.ValueString() + p.OSConfigBasePath = data.OSConfigCustomEndpoint.ValueString() + p.OSLoginBasePath = data.OSLoginCustomEndpoint.ValueString() + p.PrivatecaBasePath = data.PrivatecaCustomEndpoint.ValueString() + p.PubsubBasePath = data.PubsubCustomEndpoint.ValueString() + p.PubsubLiteBasePath = data.PubsubLiteCustomEndpoint.ValueString() + p.RedisBasePath = data.RedisCustomEndpoint.ValueString() + p.ResourceManagerBasePath = data.ResourceManagerCustomEndpoint.ValueString() + p.SecretManagerBasePath = data.SecretManagerCustomEndpoint.ValueString() + p.SecurityCenterBasePath = data.SecurityCenterCustomEndpoint.ValueString() + p.ServiceManagementBasePath = data.ServiceManagementCustomEndpoint.ValueString() + p.ServiceUsageBasePath = data.ServiceUsageCustomEndpoint.ValueString() + p.SourceRepoBasePath = data.SourceRepoCustomEndpoint.ValueString() + p.SpannerBasePath = data.SpannerCustomEndpoint.ValueString() + p.SQLBasePath = data.SQLCustomEndpoint.ValueString() + p.StorageBasePath = data.StorageCustomEndpoint.ValueString() + p.StorageTransferBasePath = data.StorageTransferCustomEndpoint.ValueString() + p.TagsBasePath = data.TagsCustomEndpoint.ValueString() + p.TPUBasePath = data.TPUCustomEndpoint.ValueString() + p.VertexAIBasePath = data.VertexAICustomEndpoint.ValueString() + p.VPCAccessBasePath = data.VPCAccessCustomEndpoint.ValueString() + p.WorkflowsBasePath = data.WorkflowsCustomEndpoint.ValueString() + + p.context = ctx + p.region = data.Region.String() + p.pollInterval = 10 * time.Second + p.project = data.Project + p.requestBatcherServiceUsage = NewRequestBatcher("Service Usage", ctx, batchingConfig) + p.requestBatcherIam = NewRequestBatcher("IAM", ctx, batchingConfig) +} + +// HandleDefaults will handle all the defaults necessary in the provider +func (p *frameworkProvider) HandleDefaults(ctx context.Context, data *ProviderModel, diags *diag.Diagnostics) { + if data.AccessToken.IsNull() && data.Credentials.IsNull() { + credentials := MultiEnvDefault([]string{ + "GOOGLE_CREDENTIALS", + "GOOGLE_CLOUD_KEYFILE_JSON", + "GCLOUD_KEYFILE_JSON", + }, nil) + + if credentials != nil { + data.Credentials = types.StringValue(credentials.(string)) + } + + accessToken := MultiEnvDefault([]string{ + "GOOGLE_OAUTH_ACCESS_TOKEN", + }, nil) + + if accessToken != nil { + data.AccessToken = types.StringValue(accessToken.(string)) + } + } + + if data.ImpersonateServiceAccount.IsNull() && os.Getenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT") != "" { + data.ImpersonateServiceAccount = types.StringValue(os.Getenv("GOOGLE_IMPERSONATE_SERVICE_ACCOUNT")) + } + + if data.Project.IsNull() { + project := MultiEnvDefault([]string{ + "GOOGLE_PROJECT", + "GOOGLE_CLOUD_PROJECT", + "GCLOUD_PROJECT", + "CLOUDSDK_CORE_PROJECT", + }, nil) + if project != nil { + data.Project = types.StringValue(project.(string)) + } + } + + if data.BillingProject.IsNull() && os.Getenv("GOOGLE_BILLING_PROJECT") != "" { + data.BillingProject = types.StringValue(os.Getenv("GOOGLE_BILLING_PROJECT")) + } + + if data.Region.IsNull() { + region := MultiEnvDefault([]string{ + "GOOGLE_REGION", + "GCLOUD_REGION", + "CLOUDSDK_COMPUTE_REGION", + }, nil) + + if region != nil { + data.Region = types.StringValue(region.(string)) + } + } + + if data.Zone.IsNull() { + zone := MultiEnvDefault([]string{ + "GOOGLE_ZONE", + "GCLOUD_ZONE", + "CLOUDSDK_COMPUTE_ZONE", + }, nil) + + if zone != nil { + data.Zone = types.StringValue(zone.(string)) + } + } + + if len(data.Scopes.Elements()) == 0 { + var d diag.Diagnostics + data.Scopes, d = types.ListValueFrom(ctx, types.StringType, defaultClientScopes) + diags.Append(d...) + if diags.HasError() { + return + } + } + + if !data.Batching.IsNull() { + var pbConfigs []ProviderBatching + d := data.Batching.ElementsAs(ctx, &pbConfigs, true) + diags.Append(d...) + if diags.HasError() { + return + } + + if pbConfigs[0].SendAfter.IsNull() { + pbConfigs[0].SendAfter = types.StringValue("10s") + } + + if pbConfigs[0].EnableBatching.IsNull() { + pbConfigs[0].EnableBatching = types.BoolValue(true) + } + + data.Batching, d = types.ListValueFrom(ctx, types.ObjectType{}, pbConfigs) + } + + if data.UserProjectOverride.IsNull() && os.Getenv("USER_PROJECT_OVERRIDE") != "" { + override, err := strconv.ParseBool(os.Getenv("USER_PROJECT_OVERRIDE")) + if err != nil { + diags.AddError( + "error parsing environment variable `USER_PROJECT_OVERRIDE` into bool", err.Error()) + } + data.UserProjectOverride = types.BoolValue(override) + } + + if data.RequestReason.IsNull() && os.Getenv("CLOUDSDK_CORE_REQUEST_REASON") != "" { + data.RequestReason = types.StringValue(os.Getenv("CLOUDSDK_CORE_REQUEST_REASON")) + } + + if data.RequestTimeout.IsNull() { + data.RequestTimeout = types.StringValue("120s") + } + + // Generated Products +<% products.each do |product| -%> + if data.<%= product[:definitions].name -%>CustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_<%= product[:definitions].name.underscore.upcase -%>_CUSTOM_ENDPOINT", + }, DefaultBasePaths[<%= product[:definitions].name -%>BasePathKey]) + if customEndpoint != nil { + data.<%= product[:definitions].name -%>CustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } +<% end -%> + + // Handwritten Products / Versioned / Atypical Entries + if data.CloudBillingCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CLOUD_BILLING_CUSTOM_ENDPOINT", + }, DefaultBasePaths["cloud_billing_custom_endpoint"]) + if customEndpoint != nil { + data.CloudBillingCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.ComposerCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_COMPOSER_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ComposerBasePathKey]) + if customEndpoint != nil { + data.ComposerCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.ContainerCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CONTAINER_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ContainerBasePathKey]) + if customEndpoint != nil { + data.ContainerCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.DataflowCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_DATAFLOW_CUSTOM_ENDPOINT", + }, DefaultBasePaths[DataflowBasePathKey]) + if customEndpoint != nil { + data.DataflowCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.IamCredentialsCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_IAM_CREDENTIALS_CUSTOM_ENDPOINT", + }, DefaultBasePaths[IamCredentialsBasePathKey]) + if customEndpoint != nil { + data.IamCredentialsCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.ResourceManagerV3CustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_RESOURCE_MANAGER_V3_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ResourceManagerV3BasePathKey]) + if customEndpoint != nil { + data.ResourceManagerV3CustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + +<% unless version == 'ga' -%> + if data.RuntimeConfigCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_RUNTIMECONFIG_CUSTOM_ENDPOINT", + }, DefaultBasePaths[RuntimeConfigBasePathKey]) + if customEndpoint != nil { + data.RuntimeConfigCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } +<% end -%> + + if data.IAMCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_IAM_CUSTOM_ENDPOINT", + }, DefaultBasePaths[IAMBasePathKey]) + if customEndpoint != nil { + data.IAMCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.ServiceNetworkingCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_SERVICE_NETWORKING_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ServiceNetworkingBasePathKey]) + if customEndpoint != nil { + data.ServiceNetworkingCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.TagsLocationCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_TAGS_LOCATION_CUSTOM_ENDPOINT", + }, DefaultBasePaths[TagsLocationBasePathKey]) + if customEndpoint != nil { + data.TagsLocationCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + // dcl + if data.ContainerAwsCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CONTAINERAWS_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ContainerAwsBasePathKey]) + if customEndpoint != nil { + data.ContainerAwsCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.ContainerAzureCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CONTAINERAZURE_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ContainerAzureBasePathKey]) + if customEndpoint != nil { + data.ContainerAzureCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + // DCL generated defaults + if data.ApikeysCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_APIKEYS_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.ApikeysCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.AssuredWorkloadsCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_ASSURED_WORKLOADS_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.AssuredWorkloadsCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.CloudBuildWorkerPoolCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CLOUD_BUILD_WORKER_POOL_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.CloudBuildWorkerPoolCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.CloudDeployCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CLOUDDEPLOY_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.CloudDeployCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.CloudResourceManagerCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_CLOUD_RESOURCE_MANAGER_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.CloudResourceManagerCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.DataplexCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_DATAPLEX_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.DataplexCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.EventarcCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_EVENTARC_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.EventarcCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.FirebaserulesCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_FIREBASERULES_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.FirebaserulesCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.NetworkConnectivityCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_NETWORK_CONNECTIVITY_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.NetworkConnectivityCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } + + if data.RecaptchaEnterpriseCustomEndpoint.IsNull() { + customEndpoint := MultiEnvDefault([]string{ + "GOOGLE_RECAPTCHA_ENTERPRISE_CUSTOM_ENDPOINT", + }, "") + if customEndpoint != nil { + data.RecaptchaEnterpriseCustomEndpoint = types.StringValue(customEndpoint.(string)) + } + } +} + +func (p *frameworkProvider) SetupClient(ctx context.Context, data ProviderModel, diags *diag.Diagnostics) { + tokenSource := GetTokenSource(ctx, data, false, diags) + if diags.HasError() { + return + } + + cleanCtx := context.WithValue(ctx, oauth2.HTTPClient, cleanhttp.DefaultClient()) + + // 1. MTLS TRANSPORT/CLIENT - sets up proper auth headers + client, _, err := transport.NewHTTPClient(cleanCtx, option.WithTokenSource(tokenSource)) + if err != nil { + diags.AddError("error creating new http client", err.Error()) + return + } + + // Userinfo is fetched before request logging is enabled to reduce additional noise. + p.logGoogleIdentities(ctx, data, diags) + if diags.HasError() { + return + } + + // 2. Logging Transport - ensure we log HTTP requests to GCP APIs. + loggingTransport := logging.NewTransport("Google", client.Transport) + + // 3. Retry Transport - retries common temporary errors + // Keep order for wrapping logging so we log each retried request as well. + // This value should be used if needed to create shallow copies with additional retry predicates. + // See ClientWithAdditionalRetries + retryTransport := NewTransportWithDefaultRetries(loggingTransport) + + // 4. Header Transport - outer wrapper to inject additional headers we want to apply + // before making requests + headerTransport := newTransportWithHeaders(retryTransport) + if !data.RequestReason.IsNull() { + headerTransport.Set("X-Goog-Request-Reason", data.RequestReason.ValueString()) + } + + // Ensure $userProject is set for all HTTP requests using the client if specified by the provider config + // See https://cloud.google.com/apis/docs/system-parameters + if data.UserProjectOverride.ValueBool() && !data.BillingProject.IsNull() { + headerTransport.Set("X-Goog-User-Project", data.BillingProject.ValueString()) + } + + // Set final transport value. + client.Transport = headerTransport + + // This timeout is a timeout per HTTP request, not per logical operation. + timeout, err := time.ParseDuration(data.RequestTimeout.ValueString()) + if err != nil { + diags.AddError("error parsing request timeout", err.Error()) + } + client.Timeout = timeout + + p.client = client +} + +func (p *frameworkProvider) SetupGrpcLogging() { + logger := logrus.StandardLogger() + + logrus.SetLevel(logrus.DebugLevel) + logrus.SetFormatter(&Formatter{ + TimestampFormat: "2006/01/02 15:04:05", + LogFormat: "%time% [%lvl%] %msg% \n", + }) + + alwaysLoggingDeciderClient := func(ctx context.Context, fullMethodName string) bool { return true } + grpc_logrus.ReplaceGrpcLogger(logrus.NewEntry(logger)) + + p.gRPCLoggingOptions = append( + p.gRPCLoggingOptions, option.WithGRPCDialOption(grpc.WithUnaryInterceptor( + grpc_logrus.PayloadUnaryClientInterceptor(logrus.NewEntry(logger), alwaysLoggingDeciderClient))), + option.WithGRPCDialOption(grpc.WithStreamInterceptor( + grpc_logrus.PayloadStreamClientInterceptor(logrus.NewEntry(logger), alwaysLoggingDeciderClient))), + ) +} + +func (p *frameworkProvider) logGoogleIdentities(ctx context.Context, data ProviderModel, diags *diag.Diagnostics) { + if data.ImpersonateServiceAccount.IsNull() { + + tokenSource := GetTokenSource(ctx, data, true, diags) + if diags.HasError() { + return + } + + p.client = oauth2.NewClient(ctx, tokenSource) // p.client isn't initialised fully when this code is called. + + email := getCurrUserEmail(p, p.userAgent, diags) + if diags.HasError() { + tflog.Info(ctx, "error retrieving userinfo for your provider credentials. have you enabled the 'https://www.googleapis.com/auth/userinfo.email' scope?") + return + } + + tflog.Info(ctx, fmt.Sprintf("Terraform is using this identity: %s", email)) + return + } + + // Drop Impersonated ClientOption from OAuth2 TokenSource to infer original identity + tokenSource := GetTokenSource(ctx, data, true, diags) + if diags.HasError() { + return + } + + p.client = oauth2.NewClient(ctx, tokenSource) // p.client isn't initialised fully when this code is called. + email := getCurrUserEmail(p, p.userAgent, diags) + if diags.HasError() { + tflog.Info(ctx, "error retrieving userinfo for your provider credentials. have you enabled the 'https://www.googleapis.com/auth/userinfo.email' scope?") + return + } + + tflog.Info(ctx, fmt.Sprintf("Terraform is configured with service account impersonation, original identity: %s, impersonated identity: %s", email, data.ImpersonateServiceAccount.ValueString())) + + // Add the Impersonated ClientOption back in to the OAuth2 TokenSource + tokenSource = GetTokenSource(ctx, data, false, diags) + if diags.HasError() { + return + } + + p.client = oauth2.NewClient(ctx, tokenSource) // p.client isn't initialised fully when this code is called. + + return +} + +// Configuration helpers + +// GetTokenSource gets token source based on the Google Credentials configured. +// If initialCredentialsOnly is true, don't follow the impersonation settings and return the initial set of creds. +func GetTokenSource(ctx context.Context, data ProviderModel, initialCredentialsOnly bool, diags *diag.Diagnostics) oauth2.TokenSource { + creds := GetCredentials(ctx, data, initialCredentialsOnly, diags) + + return creds.TokenSource +} + +// GetCredentials gets credentials with a given scope (clientScopes). +// If initialCredentialsOnly is true, don't follow the impersonation +// settings and return the initial set of creds instead. +func GetCredentials(ctx context.Context, data ProviderModel, initialCredentialsOnly bool, diags *diag.Diagnostics) googleoauth.Credentials { + var clientScopes []string + var delegates []string + + d := data.Scopes.ElementsAs(ctx, &clientScopes, false) + diags.Append(d...) + if diags.HasError() { + return googleoauth.Credentials{} + } + + d = data.ImpersonateServiceAccountDelegates.ElementsAs(ctx, &delegates, false) + diags.Append(d...) + if diags.HasError() { + return googleoauth.Credentials{} + } + + if !data.AccessToken.IsNull() { + contents, _, err := pathOrContents(data.AccessToken.ValueString()) + if err != nil { + diags.AddError("error loading access token", err.Error()) + return googleoauth.Credentials{} + } + + token := &oauth2.Token{AccessToken: contents} + if !data.ImpersonateServiceAccount.IsNull() && !initialCredentialsOnly { + opts := []option.ClientOption{option.WithTokenSource(oauth2.StaticTokenSource(token)), option.ImpersonateCredentials(data.ImpersonateServiceAccount.ValueString(), delegates...), option.WithScopes(clientScopes...)} + creds, err := transport.Creds(context.TODO(), opts...) + if err != nil { + diags.AddError("error impersonating credentials", err.Error()) + return googleoauth.Credentials{} + } + return *creds + } + + tflog.Info(ctx, "Authenticating using configured Google JSON 'access_token'...") + tflog.Info(ctx, fmt.Sprintf(" -- Scopes: %s", clientScopes)) + return googleoauth.Credentials{ + TokenSource: staticTokenSource{oauth2.StaticTokenSource(token)}, + } + } + + if !data.Credentials.IsNull() { + contents, _, err := pathOrContents(data.Credentials.ValueString()) + if err != nil { + diags.AddError(fmt.Sprintf("error loading credentials: %s", err), err.Error()) + return googleoauth.Credentials{} + } + + if !data.ImpersonateServiceAccount.IsNull() && !initialCredentialsOnly { + opts := []option.ClientOption{option.WithCredentialsJSON([]byte(contents)), option.ImpersonateCredentials(data.ImpersonateServiceAccount.ValueString(), delegates...), option.WithScopes(clientScopes...)} + creds, err := transport.Creds(context.TODO(), opts...) + if err != nil { + diags.AddError("error impersonating credentials", err.Error()) + return googleoauth.Credentials{} + } + return *creds + } + + creds, err := googleoauth.CredentialsFromJSON(ctx, []byte(contents), clientScopes...) + if err != nil { + diags.AddError("unable to parse credentials", err.Error()) + return googleoauth.Credentials{} + } + + tflog.Info(ctx, "Authenticating using configured Google JSON 'credentials'...") + tflog.Info(ctx, fmt.Sprintf(" -- Scopes: %s", clientScopes)) + return *creds + } + + if !data.ImpersonateServiceAccount.IsNull() && !initialCredentialsOnly { + opts := option.ImpersonateCredentials(data.ImpersonateServiceAccount.ValueString(), delegates...) + creds, err := transport.Creds(context.TODO(), opts, option.WithScopes(clientScopes...)) + if err != nil { + diags.AddError("error impersonating credentials", err.Error()) + return googleoauth.Credentials{} + } + + return *creds + } + + tflog.Info(ctx, "Authenticating using DefaultClient...") + tflog.Info(ctx, fmt.Sprintf(" -- Scopes: %s", clientScopes)) + defaultTS, err := googleoauth.DefaultTokenSource(context.Background(), clientScopes...) + if err != nil { + diags.AddError(fmt.Sprintf("Attempted to load application default credentials since neither `credentials` nor `access_token` was set in the provider block. "+ + "No credentials loaded. To use your gcloud credentials, run 'gcloud auth application-default login'"), err.Error()) + return googleoauth.Credentials{} + } + + return googleoauth.Credentials{ + TokenSource: defaultTS, + } +} + +// GetBatchingConfig returns the batching config object given the +// provider configuration set for batching +func GetBatchingConfig(ctx context.Context, data types.List, diags *diag.Diagnostics) *batchingConfig { + bc := &batchingConfig{ + sendAfter: time.Second * defaultBatchSendIntervalSec, + enableBatching: true, + } + + if data.IsNull() { + return bc + } + + var pbConfigs []ProviderBatching + d := data.ElementsAs(ctx, &pbConfigs, true) + diags.Append(d...) + if diags.HasError() { + return bc + } + + sendAfter, err := time.ParseDuration(pbConfigs[0].SendAfter.ValueString()) + if err != nil { + diags.AddError("error parsing send after time duration", err.Error()) + return bc + } + + bc.sendAfter = sendAfter + + if !pbConfigs[0].EnableBatching.IsNull() { + bc.enableBatching = pbConfigs[0].EnableBatching.ValueBool() + } + + return bc +} diff --git a/mmv1/third_party/terraform/framework_utils/framework_provider_test.go b/mmv1/third_party/terraform/framework_utils/framework_provider_test.go new file mode 100644 index 000000000000..1f6de45d24bb --- /dev/null +++ b/mmv1/third_party/terraform/framework_utils/framework_provider_test.go @@ -0,0 +1,242 @@ +package google + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/dnaeon/go-vcr/cassette" + "github.com/dnaeon/go-vcr/recorder" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +var fwProviders map[string]*frameworkTestProvider + +type frameworkTestProvider struct { + ProdProvider frameworkProvider + TestName string +} + +func NewFrameworkTestProvider(testName string) *frameworkTestProvider { + return &frameworkTestProvider{ + ProdProvider: frameworkProvider{ + version: "test", + }, + TestName: testName, + } +} + +// Configure is here to overwrite the frameworkProvider configure function for VCR testing +func (p *frameworkTestProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + if isVcrEnabled() { + configsLock.RLock() + _, ok := fwProviders[p.TestName] + configsLock.RUnlock() + if ok { + return + } + p.ProdProvider.Configure(ctx, req, resp) + if resp.Diagnostics.HasError() { + return + } + var vcrMode recorder.Mode + switch vcrEnv := os.Getenv("VCR_MODE"); vcrEnv { + case "RECORDING": + vcrMode = recorder.ModeRecording + case "REPLAYING": + vcrMode = recorder.ModeReplaying + // When replaying, set the poll interval low to speed up tests + p.ProdProvider.pollInterval = 10 * time.Millisecond + default: + tflog.Debug(ctx, fmt.Sprintf("No valid environment var set for VCR_MODE, expected RECORDING or REPLAYING, skipping VCR. VCR_MODE: %s", vcrEnv)) + return + } + + envPath := os.Getenv("VCR_PATH") + if envPath == "" { + tflog.Debug(ctx, "No environment var set for VCR_PATH, skipping VCR") + return + } + path := filepath.Join(envPath, vcrFileName(p.TestName)) + + rec, err := recorder.NewAsMode(path, vcrMode, p.ProdProvider.client.Transport) + if err != nil { + resp.Diagnostics.AddError("error creating record as new mode", err.Error()) + return + } + // Defines how VCR will match requests to responses. + rec.SetMatcher(func(r *http.Request, i cassette.Request) bool { + // Default matcher compares method and URL only + if !cassette.DefaultMatcher(r, i) { + return false + } + if r.Body == nil { + return true + } + contentType := r.Header.Get("Content-Type") + // If body contains media, don't try to compare + if strings.Contains(contentType, "multipart/related") { + return true + } + + var b bytes.Buffer + if _, err := b.ReadFrom(r.Body); err != nil { + tflog.Debug(ctx, fmt.Sprintf("Failed to read request body from cassette: %v", err)) + return false + } + r.Body = ioutil.NopCloser(&b) + reqBody := b.String() + // If body matches identically, we are done + if reqBody == i.Body { + return true + } + + // JSON might be the same, but reordered. Try parsing json and comparing + if strings.Contains(contentType, "application/json") { + var reqJson, cassetteJson interface{} + if err := json.Unmarshal([]byte(reqBody), &reqJson); err != nil { + tflog.Debug(ctx, fmt.Sprintf("Failed to unmarshall request json: %v", err)) + return false + } + if err := json.Unmarshal([]byte(i.Body), &cassetteJson); err != nil { + tflog.Debug(ctx, fmt.Sprintf("Failed to unmarshall cassette json: %v", err)) + return false + } + return reflect.DeepEqual(reqJson, cassetteJson) + } + return false + }) + p.ProdProvider.client.Transport = rec + configsLock.Lock() + fwProviders[p.TestName] = p + configsLock.Unlock() + return + } else { + tflog.Debug(ctx, "VCR_PATH or VCR_MODE not set, skipping VCR") + } +} + +func configureApiClient(ctx context.Context, p *frameworkTestProvider, diags *diag.Diagnostics) { + var data ProviderModel + var d diag.Diagnostics + + // Set defaults if needed - the only attribute without a default is ImpersonateServiceAccountDelegates + // this is a bit of a hack, but we'll just initialize it here so that it's been initialized at least + data.ImpersonateServiceAccountDelegates, d = types.ListValue(types.StringType, []attr.Value{}) + diags.Append(d...) + if diags.HasError() { + return + } + p.ProdProvider.ConfigureWithData(ctx, data, "test", diags) +} + +func getTestAccFrameworkProviders(testName string, c resource.TestCase) map[string]func() (tfprotov5.ProviderServer, error) { + myFunc := func() (tfprotov5.ProviderServer, error) { + prov, err := MuxedProviders(testName) + return prov(), err + } + + var testProvider string + providerMapKeys := reflect.ValueOf(c.ProtoV5ProviderFactories).MapKeys() + if len(providerMapKeys) > 0. { + if strings.Contains(providerMapKeys[0].String(), "google-beta") { + testProvider = "google-beta" + } else { + testProvider = "google" + } + return map[string]func() (tfprotov5.ProviderServer, error){ + testProvider: myFunc, + } + } + return map[string]func() (tfprotov5.ProviderServer, error){} +} + +func getTestFwProvider(t *testing.T) *frameworkTestProvider { + configsLock.RLock() + fwProvider, ok := fwProviders[t.Name()] + configsLock.RUnlock() + if ok { + return fwProvider + } + + var diags diag.Diagnostics + p := NewFrameworkTestProvider(t.Name()) + configureApiClient(context.Background(), p, &diags) + if diags.HasError() { + log.Fatalf("%d errors when configuring test provider client: first is %s", diags.ErrorsCount(), diags.Errors()[0].Detail()) + } + + return p +} + +func TestAccFrameworkProviderMeta_setModuleName(t *testing.T) { + t.Parallel() + + moduleName := "my-module" + managedZoneName := fmt.Sprintf("tf-test-zone-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "google": func() (tfprotov5.ProviderServer, error) { + provider, err := MuxedProviders(t.Name()) + return provider(), err + }, + }, + // CheckDestroy: testAccCheckComputeAddressDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccFrameworkProviderMeta_setModuleName(moduleName, managedZoneName, randString(t, 10)), + }, + }, + }) +} + +func testAccFrameworkProviderMeta_setModuleName(key, managedZoneName, recordSetName string) string { + return fmt.Sprintf(` +terraform { + provider_meta "google" { + module_name = "%s" + } +} + + +provider "google" {} + +resource "google_dns_managed_zone" "zone" { + name = "test-zone" + dns_name = "%s.hashicorptest.com." +} + +resource "google_dns_record_set" "rs" { + managed_zone = google_dns_managed_zone.zone.name + name = "%s.${google_dns_managed_zone.zone.dns_name}" + type = "A" + ttl = 300 + rrdatas = [ + "192.168.1.0", + ] +} + +data "google_dns_record_set" "rs" { + managed_zone = google_dns_record_set.rs.managed_zone + name = google_dns_record_set.rs.name + type = google_dns_record_set.rs.type +}`, key, managedZoneName, recordSetName) +} diff --git a/mmv1/third_party/terraform/framework_utils/framework_test_utils.go b/mmv1/third_party/terraform/framework_utils/framework_test_utils.go new file mode 100644 index 000000000000..2f1fda70b198 --- /dev/null +++ b/mmv1/third_party/terraform/framework_utils/framework_test_utils.go @@ -0,0 +1,95 @@ +package google + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// General test utils +func MuxedProviders(testName string) (func() tfprotov5.ProviderServer, error) { + ctx := context.Background() + + providers := []func() tfprotov5.ProviderServer{ + providerserver.NewProtocol5(New("test")), // framework provider + Provider().GRPCProvider, // sdk provider + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) + + if err != nil { + return nil, err + } + + return muxServer.ProviderServer, nil +} + +// testExtractResourceAttr navigates a test's state to find the specified resource (or data source) attribute and makes the value +// accessible via the attributeValue string pointer. +func testExtractResourceAttr(resourceName string, attributeName string, attributeValue *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] // To find a datasource, include `data.` at the start of the resourceName value + + if !ok { + return fmt.Errorf("resource name %s not found in state", resourceName) + } + + attrValue, ok := rs.Primary.Attributes[attributeName] + + if !ok { + return fmt.Errorf("attribute %s not found in resource %s state", attributeName, resourceName) + } + + *attributeValue = attrValue + + return nil + } +} + +// testCheckAttributeValuesEqual compares two string pointers, which have been used to retrieve attribute values from the test's state. +func testCheckAttributeValuesEqual(i *string, j *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if testStringValue(i) != testStringValue(j) { + return fmt.Errorf("attribute values are different, got %s and %s", testStringValue(i), testStringValue(j)) + } + + return nil + } +} + +// testStringValue returns string values from string pointers, handling nil pointers. +func testStringValue(sPtr *string) string { + if sPtr == nil { + return "" + } + + return *sPtr +} + +// protoV5ProviderFactories returns a muxed ProviderServer that uses the provider code from this repo (SDK and plugin-framework). +// Used to set ProtoV5ProviderFactories in a resource.TestStep within an acceptance test. +func protoV5ProviderFactories(t *testing.T) map[string]func() (tfprotov5.ProviderServer, error) { + return map[string]func() (tfprotov5.ProviderServer, error){ + "google": func() (tfprotov5.ProviderServer, error) { + provider, err := MuxedProviders(t.Name()) + return provider(), err + }, + } +} + +// providerVersion450 returns information allowing tests to download TPG v4.50.0 from the Registry during `init` +// Used to set ExternalProviders in a resource.TestStep within an acceptance test. +func providerVersion450() map[string]resource.ExternalProvider { + return map[string]resource.ExternalProvider{ + "google": { + VersionConstraint: "4.50.0", + Source: "hashicorp/google", + }, + } +} diff --git a/mmv1/third_party/terraform/framework_utils/framework_transport.go b/mmv1/third_party/terraform/framework_utils/framework_transport.go new file mode 100644 index 000000000000..6b0f0b5f1ddc --- /dev/null +++ b/mmv1/third_party/terraform/framework_utils/framework_transport.go @@ -0,0 +1,102 @@ +package google + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + + "google.golang.org/api/googleapi" +) + +func sendFrameworkRequest(p *frameworkProvider, method, project, rawurl, userAgent string, body map[string]interface{}, errorRetryPredicates ...RetryErrorPredicateFunc) (map[string]interface{}, diag.Diagnostics) { + return sendFrameworkRequestWithTimeout(p, method, project, rawurl, userAgent, body, DefaultRequestTimeout, errorRetryPredicates...) +} + +func sendFrameworkRequestWithTimeout(p *frameworkProvider, method, project, rawurl, userAgent string, body map[string]interface{}, timeout time.Duration, errorRetryPredicates ...RetryErrorPredicateFunc) (map[string]interface{}, diag.Diagnostics) { + var diags diag.Diagnostics + + reqHeaders := make(http.Header) + reqHeaders.Set("User-Agent", userAgent) + reqHeaders.Set("Content-Type", "application/json") + + if p.userProjectOverride && project != "" { + // When project is "NO_BILLING_PROJECT_OVERRIDE" in the function GetCurrentUserEmail, + // set the header X-Goog-User-Project to be empty string. + if project == "NO_BILLING_PROJECT_OVERRIDE" { + reqHeaders.Set("X-Goog-User-Project", "") + } else { + // Pass the project into this fn instead of parsing it from the URL because + // both project names and URLs can have colons in them. + reqHeaders.Set("X-Goog-User-Project", project) + } + } + + if timeout == 0 { + timeout = time.Hour + } + + var res *http.Response + err := retryTimeDuration( + func() error { + var buf bytes.Buffer + if body != nil { + err := json.NewEncoder(&buf).Encode(body) + if err != nil { + return err + } + } + + u, err := addQueryParams(rawurl, map[string]string{"alt": "json"}) + if err != nil { + return err + } + req, err := http.NewRequest(method, u, &buf) + if err != nil { + return err + } + + req.Header = reqHeaders + res, err = p.client.Do(req) + if err != nil { + return err + } + + if err := googleapi.CheckResponse(res); err != nil { + googleapi.CloseBody(res) + return err + } + + return nil + }, + timeout, + errorRetryPredicates..., + ) + if err != nil { + diags.AddError("error sending request", err.Error()) + return nil, diags + } + + if res == nil { + diags.AddError("Unable to parse server response.", "This is most likely a terraform problem, please file a bug at https://github.com/hashicorp/terraform-provider-google/issues.") + return nil, diags + } + + // The defer call must be made outside of the retryFunc otherwise it's closed too soon. + defer googleapi.CloseBody(res) + + // 204 responses will have no body, so we're going to error with "EOF" if we + // try to parse it. Instead, we can just return nil. + if res.StatusCode == 204 { + return nil, diags + } + result := make(map[string]interface{}) + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + diags.AddError("error decoding response body", err.Error()) + return nil, diags + } + + return result, diags +} diff --git a/mmv1/third_party/terraform/framework_utils/framework_utils.go b/mmv1/third_party/terraform/framework_utils/framework_utils.go new file mode 100644 index 000000000000..5670c1a71fd0 --- /dev/null +++ b/mmv1/third_party/terraform/framework_utils/framework_utils.go @@ -0,0 +1,139 @@ +package google + +import ( + "context" + "fmt" + "os" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +const uaEnvVar = "TF_APPEND_USER_AGENT" + +func CompileUserAgentString(ctx context.Context, name, tfVersion, provVersion string) string { + ua := fmt.Sprintf("Terraform/%s (+https://www.terraform.io) Terraform-Plugin-SDK/%s %s/%s", tfVersion, "terraform-plugin-framework", name, provVersion) + + if add := os.Getenv(uaEnvVar); add != "" { + add = strings.TrimSpace(add) + if len(add) > 0 { + ua += " " + add + tflog.Debug(ctx, fmt.Sprintf("Using modified User-Agent: %s", ua)) + } + } + + return ua +} + +func getCurrUserEmail(p *frameworkProvider, userAgent string, diags *diag.Diagnostics) string { + // When environment variables UserProjectOverride and BillingProject are set for the provider, + // the header X-Goog-User-Project is set for the API requests. + // But it causes an error when calling GetCurrUserEmail. Set the project to be "NO_BILLING_PROJECT_OVERRIDE". + // And then it triggers the header X-Goog-User-Project to be set to empty string. + + // See https://github.com/golang/oauth2/issues/306 for a recommendation to do this from a Go maintainer + // URL retrieved from https://accounts.google.com/.well-known/openid-configuration + res, d := sendFrameworkRequest(p, "GET", "NO_BILLING_PROJECT_OVERRIDE", "https://openidconnect.googleapis.com/v1/userinfo", userAgent, nil) + diags.Append(d...) + + if diags.HasError() { + tflog.Info(p.context, "error retrieving userinfo for your provider credentials. have you enabled the 'https://www.googleapis.com/auth/userinfo.email' scope?") + return "" + } + if res["email"] == nil { + diags.AddError("error retrieving email from userinfo.", "email was nil in the response.") + return "" + } + return res["email"].(string) +} + +func generateFrameworkUserAgentString(metaData *ProviderMetaModel, currUserAgent string) string { + if metaData != nil && !metaData.ModuleName.IsNull() && metaData.ModuleName.ValueString() != "" { + return strings.Join([]string{currUserAgent, metaData.ModuleName.ValueString()}, " ") + } + + return currUserAgent +} + +// getProject reads the "project" field from the given resource and falls +// back to the provider's value if not given. If the provider's value is not +// given, an error is returned. +func getProjectFramework(rVal, pVal types.String, diags *diag.Diagnostics) types.String { + return getProjectFromSchemaFramework("project", rVal, pVal, diags) +} + +func getProjectFromSchemaFramework(projectSchemaField string, rVal, pVal types.String, diags *diag.Diagnostics) types.String { + if !rVal.IsNull() && rVal.ValueString() != "" { + return rVal + } + + if !pVal.IsNull() && pVal.ValueString() != "" { + return pVal + } + + diags.AddError("required field is not set", fmt.Sprintf("%s is not set", projectSchemaField)) + return types.String{} +} + +func handleDatasourceNotFoundError(ctx context.Context, err error, state *tfsdk.State, resource string, diags *diag.Diagnostics) { + if isGoogleApiErrorWithCode(err, 404) { + tflog.Warn(ctx, fmt.Sprintf("Removing %s because it's gone", resource)) + // The resource doesn't exist anymore + state.RemoveResource(ctx) + } + + diags.AddError(fmt.Sprintf("Error when reading or editing %s", resource), err.Error()) +} + +// field helpers + +// Parses a project field with the following formats: +// - projects/{my_projects}/{resource_type}/{resource_name} +func parseProjectFieldValueFramework(resourceType, fieldValue, projectSchemaField string, rVal, pVal types.String, isEmptyValid bool, diags *diag.Diagnostics) *ProjectFieldValue { + if len(fieldValue) == 0 { + if isEmptyValid { + return &ProjectFieldValue{resourceType: resourceType} + } + diags.AddError("field can not be empty", fmt.Sprintf("The project field for resource %s cannot be empty", resourceType)) + return nil + } + + r := regexp.MustCompile(fmt.Sprintf(projectBasePattern, resourceType)) + if parts := r.FindStringSubmatch(fieldValue); parts != nil { + return &ProjectFieldValue{ + Project: parts[1], + Name: parts[2], + + resourceType: resourceType, + } + } + + project := getProjectFromFrameworkSchema(projectSchemaField, rVal, pVal, diags) + if diags.HasError() { + return nil + } + + return &ProjectFieldValue{ + Project: project.ValueString(), + Name: GetResourceNameFromSelfLink(fieldValue), + + resourceType: resourceType, + } +} + +func getProjectFromFrameworkSchema(projectSchemaField string, rVal, pVal types.String, diags *diag.Diagnostics) types.String { + if !rVal.IsNull() && projectSchemaField != "" { + return rVal + } + + if !pVal.IsNull() { + return pVal + } + + diags.AddError("required field is not set", fmt.Sprintf("%s must be set", projectSchemaField)) + return types.String{} +} diff --git a/mmv1/third_party/terraform/framework_utils/framework_validators.go b/mmv1/third_party/terraform/framework_utils/framework_validators.go new file mode 100644 index 000000000000..2391b1a221fd --- /dev/null +++ b/mmv1/third_party/terraform/framework_utils/framework_validators.go @@ -0,0 +1,92 @@ +package google + +import ( + "context" + "fmt" + "os" + "regexp" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + googleoauth "golang.org/x/oauth2/google" +) + +func CustomEndpointValidator() validator.String { + return stringvalidator.RegexMatches(regexp.MustCompile(`.*/[^/]+/$`), "") +} + +// Credentials Validator +var _ validator.String = credentialsValidator{} + +// credentialsValidator validates that a string Attribute's is valid JSON credentials. +type credentialsValidator struct { +} + +// Description describes the validation in plain text formatting. +func (v credentialsValidator) Description(_ context.Context) string { + return "value must be a path to valid JSON credentials or valid, raw, JSON credentials" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v credentialsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateString performs the validation. +func (v credentialsValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue.ValueString() + + // if this is a path and we can stat it, assume it's ok + if _, err := os.Stat(value); err == nil { + return + } + if _, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(value)); err != nil { + response.Diagnostics.AddError("JSON credentials are not valid", err.Error()) + } +} + +func CredentialsValidator() validator.String { + return credentialsValidator{} +} + +// Non Negative Duration Validator +type nonnegativedurationValidator struct { +} + +// Description describes the validation in plain text formatting. +func (v nonnegativedurationValidator) Description(_ context.Context) string { + return "value expected to be a string representing a non-negative duration" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v nonnegativedurationValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateString performs the validation. +func (v nonnegativedurationValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + value := request.ConfigValue.ValueString() + dur, err := time.ParseDuration(value) + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("expected %s to be a duration", value), err.Error()) + return + } + + if dur < 0 { + response.Diagnostics.AddError("duration must be non-negative", fmt.Sprintf("duration provided: %d", dur)) + } +} + +func NonNegativeDurationValidator() validator.String { + return nonnegativedurationValidator{} +} diff --git a/mmv1/third_party/terraform/go.mod.erb b/mmv1/third_party/terraform/go.mod.erb index 4fcf2f0a11f5..e3a22296fc68 100644 --- a/mmv1/third_party/terraform/go.mod.erb +++ b/mmv1/third_party/terraform/go.mod.erb @@ -16,6 +16,11 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 + github.com/hashicorp/terraform-plugin-framework v1.1.1 + github.com/hashicorp/terraform-plugin-framework-validators v0.9.0 + github.com/hashicorp/terraform-plugin-go v0.14.3 + github.com/hashicorp/terraform-plugin-log v0.7.0 + github.com/hashicorp/terraform-plugin-mux v0.8.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/hashstructure v1.1.0 @@ -56,16 +61,14 @@ require ( github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/go-plugin v1.4.4 // indirect + github.com/hashicorp/go-plugin v1.4.8 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/hc-install v0.4.0 // indirect github.com/hashicorp/hcl/v2 v2.14.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.17.3 // indirect github.com/hashicorp/terraform-json v0.14.0 // indirect - github.com/hashicorp/terraform-plugin-go v0.14.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect - github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c // indirect + github.com/hashicorp/terraform-registry-address v0.1.0 // indirect github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/jhump/protoreflect v1.6.1 // indirect @@ -80,7 +83,6 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect - github.com/vmihailenco/tagparser v0.1.1 // indirect github.com/zclconf/go-cty v1.11.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect diff --git a/mmv1/third_party/terraform/go.sum b/mmv1/third_party/terraform/go.sum index 5f15759e2060..8e09cfeabd70 100644 --- a/mmv1/third_party/terraform/go.sum +++ b/mmv1/third_party/terraform/go.sum @@ -172,14 +172,28 @@ github.com/hashicorp/terraform-exec v0.17.3 h1:MX14Kvnka/oWGmIkyuyvL6POx25ZmKrjl github.com/hashicorp/terraform-exec v0.17.3/go.mod h1:+NELG0EqQekJzhvikkeQsOAZpsw0cv/03rbeQJqscAI= github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s= github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= +github.com/hashicorp/terraform-plugin-framework v1.0.1 h1:apX2jtaEKa15+do6H2izBJdl1dEH2w5BPVkDJ3Q3mKA= +github.com/hashicorp/terraform-plugin-framework v1.0.1/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg= +github.com/hashicorp/terraform-plugin-framework v1.1.1 h1:PbnEKHsIU8KTTzoztHQGgjZUWx7Kk8uGtpGMMc1p+oI= +github.com/hashicorp/terraform-plugin-framework v1.1.1/go.mod h1:DyZPxQA+4OKK5ELxFIIcqggcszqdWWUpTLPHAhS/tkY= +github.com/hashicorp/terraform-plugin-framework-validators v0.9.0 h1:LYz4bXh3t7bTEydXOmPDPupRRnA480B/9+jV8yZvxBA= +github.com/hashicorp/terraform-plugin-framework-validators v0.9.0/go.mod h1:+BVERsnfdlhYR2YkXMBtPnmn9UsL19U3qUtSZ+Y/5MY= github.com/hashicorp/terraform-plugin-go v0.14.0 h1:ttnSlS8bz3ZPYbMb84DpcPhY4F5DsQtcAS7cHo8uvP4= github.com/hashicorp/terraform-plugin-go v0.14.0/go.mod h1:2nNCBeRLaenyQEi78xrGrs9hMbulveqG/zDMQSvVJTE= +github.com/hashicorp/terraform-plugin-go v0.14.2 h1:rhsVEOGCnY04msNymSvbUsXfRLKh9znXZmHlf5e8mhE= +github.com/hashicorp/terraform-plugin-go v0.14.2/go.mod h1:Q12UjumPNGiFsZffxOsA40Tlz1WVXt2Evh865Zj0+UA= +github.com/hashicorp/terraform-plugin-go v0.14.3 h1:nlnJ1GXKdMwsC8g1Nh05tK2wsC3+3BL/DBBxFEki+j0= +github.com/hashicorp/terraform-plugin-go v0.14.3/go.mod h1:7ees7DMZ263q8wQ6E4RdIdR6nHHJtrdt4ogX5lPkX1A= github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs= github.com/hashicorp/terraform-plugin-log v0.7.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= +github.com/hashicorp/terraform-plugin-mux v0.8.0 h1:WCTP66mZ+iIaIrCNJnjPEYnVjawTshnDJu12BcXK1EI= +github.com/hashicorp/terraform-plugin-mux v0.8.0/go.mod h1:vdW0daEi8Kd4RFJmet5Ot+SIVB/B8SwQVJiYKQwdCy8= github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.0 h1:FtCLTiTcykdsURXPt/ku7fYXm3y19nbzbZcUxHx9RbI= github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.0/go.mod h1:80wf5oad1tW+oLnbXS4UTYmDCrl7BuN1Q+IA91X1a4Y= github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c h1:D8aRO6+mTqHfLsK/BC3j5OAoogv1WLRWzY1AaTo3rBg= github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI= +github.com/hashicorp/terraform-registry-address v0.1.0 h1:W6JkV9wbum+m516rCl5/NjKxCyTVaaUBbzYcMzBDO3U= +github.com/hashicorp/terraform-registry-address v0.1.0/go.mod h1:EnyO2jYO6j29DTHbJcm00E5nQTFeTtyZH3H5ycydQ5A= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= @@ -249,6 +263,7 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -267,6 +282,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvC github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/mmv1/third_party/terraform/main.go.erb b/mmv1/third_party/terraform/main.go.erb index 46a3e6b3d89c..85aa07d62517 100644 --- a/mmv1/third_party/terraform/main.go.erb +++ b/mmv1/third_party/terraform/main.go.erb @@ -2,11 +2,58 @@ package main import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + "context" + "flag" + "log" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" "github.com/hashicorp/terraform-provider-google<%= "-" + version unless version == 'ga' -%>/google<%= "-" + version unless version == 'ga' -%>" + ver "github.com/hashicorp/terraform-provider-google<%= "-" + version unless version == 'ga' -%>/version" +) + +var ( + // these will be set by the goreleaser configuration + // to appropriate values for the compiled binary + version string = ver.ProviderVersion + + // goreleaser can also pass the specific commit if you want + // commit string = "" ) func main() { - plugin.Serve(&plugin.ServeOpts{ - ProviderFunc: google.Provider}) + var debug bool + + flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + + // concat with sdkv2 provider + providers := []func() tfprotov5.ProviderServer{ + providerserver.NewProtocol5(google.New(version)), // framework provider + google.Provider().GRPCProvider, // sdk provider + } + + // use the muxer + muxServer, err := tf5muxserver.NewMuxServer(context.Background(), providers...) + if err != nil { + log.Fatalf(err.Error()) + } + + var serveOpts []tf5server.ServeOpt + + if debug { + serveOpts = append(serveOpts, tf5server.WithManagedDebug()) + } + + err = tf5server.Serve( + "registry.terraform.io/hashicorp/google<%= "-" + version unless version == 'ga' -%>", + muxServer.ProviderServer, + serveOpts..., + ) + + if err != nil { + log.Fatal(err) + } } diff --git a/mmv1/third_party/terraform/tests/data_source_dns_key_test.go b/mmv1/third_party/terraform/tests/data_source_dns_key_test.go index 2c8e25403e8a..9e4e8a54ff95 100644 --- a/mmv1/third_party/terraform/tests/data_source_dns_key_test.go +++ b/mmv1/third_party/terraform/tests/data_source_dns_key_test.go @@ -11,21 +11,41 @@ import ( func TestAccDataSourceDNSKeys_basic(t *testing.T) { t.Parallel() - dnsZoneName := fmt.Sprintf("data-dnskey-test-%s", randString(t, 10)) + dnsZoneName := fmt.Sprintf("tf-dnskey-test-%s", randString(t, 10)) + + var kskDigest1, kskDigest2, zskPubKey1, zskPubKey2, kskAlg1, kskAlg2 string vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDNSManagedZoneDestroyProducer(t), + CheckDestroy: testAccCheckDNSManagedZoneDestroyProducerFramework(t), Steps: []resource.TestStep{ { - Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "on"), + ExternalProviders: providerVersion450(), + Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "on"), Check: resource.ComposeTestCheckFunc( testAccDataSourceDNSKeysDSRecordCheck("data.google_dns_keys.foo_dns_key"), resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "1"), resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "1"), resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key_id", "key_signing_keys.#", "1"), resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key_id", "zone_signing_keys.#", "1"), + testExtractResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.0.digests.0.digest", &kskDigest1), + testExtractResourceAttr("data.google_dns_keys.foo_dns_key_id", "zone_signing_keys.0.public_key", &zskPubKey1), + testExtractResourceAttr("data.google_dns_keys.foo_dns_key_id", "key_signing_keys.0.algorithm", &kskAlg1), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(t), + Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "on"), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceDNSKeysDSRecordCheck("data.google_dns_keys.foo_dns_key"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "1"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "1"), + testExtractResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.0.digests.0.digest", &kskDigest2), + testExtractResourceAttr("data.google_dns_keys.foo_dns_key_id", "zone_signing_keys.0.public_key", &zskPubKey2), + testExtractResourceAttr("data.google_dns_keys.foo_dns_key_id", "key_signing_keys.0.algorithm", &kskAlg2), + testCheckAttributeValuesEqual(&kskDigest1, &kskDigest2), + testCheckAttributeValuesEqual(&zskPubKey1, &zskPubKey2), + testCheckAttributeValuesEqual(&kskAlg1, &kskAlg2), ), }, }, @@ -35,15 +55,23 @@ func TestAccDataSourceDNSKeys_basic(t *testing.T) { func TestAccDataSourceDNSKeys_noDnsSec(t *testing.T) { t.Parallel() - dnsZoneName := fmt.Sprintf("data-dnskey-test-%s", randString(t, 10)) + dnsZoneName := fmt.Sprintf("tf-dnskey-test-%s", randString(t, 10)) vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDNSManagedZoneDestroyProducer(t), + CheckDestroy: testAccCheckDNSManagedZoneDestroyProducerFramework(t), Steps: []resource.TestStep{ { - Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "off"), + ExternalProviders: providerVersion450(), + Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "off"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "0"), + resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(t), + Config: testAccDataSourceDNSKeysConfig(dnsZoneName, "off"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "key_signing_keys.#", "0"), resource.TestCheckResourceAttr("data.google_dns_keys.foo_dns_key", "zone_signing_keys.#", "0"), diff --git a/mmv1/third_party/terraform/tests/data_source_dns_managed_zone_test.go.erb b/mmv1/third_party/terraform/tests/data_source_dns_managed_zone_test.go.erb index 27a474c5690a..c7d496652579 100644 --- a/mmv1/third_party/terraform/tests/data_source_dns_managed_zone_test.go.erb +++ b/mmv1/third_party/terraform/tests/data_source_dns_managed_zone_test.go.erb @@ -3,9 +3,11 @@ package google import ( "fmt" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccDataSourceDnsManagedZone_basic(t *testing.T) { @@ -13,24 +15,50 @@ func TestAccDataSourceDnsManagedZone_basic(t *testing.T) { vcrTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDNSManagedZoneDestroyProducer(t), + CheckDestroy: testAccCheckDNSManagedZoneDestroyProducerFramework(t), Steps: []resource.TestStep{ { + ExternalProviders: providerVersion450(), + Config: testAccDataSourceDnsManagedZone_basic(randString(t, 10)), + Check: checkDataSourceStateMatchesResourceStateWithIgnores( + "data.google_dns_managed_zone.qa", + "google_dns_managed_zone.foo", + map[string]struct{}{ + "dnssec_config.#": {}, + "private_visibility_config.#": {}, + "peering_config.#": {}, + "forwarding_config.#": {}, + "force_destroy": {}, + "labels.#": {}, + "creation_time": {}, + "cloud_logging_config.#": {}, + "cloud_logging_config.0.%": {}, + "cloud_logging_config.0.enable_logging": {}, +<% unless version == "ga" -%> + "reverse_lookup": {}, +<% end -%> + }, + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(t), Config: testAccDataSourceDnsManagedZone_basic(randString(t, 10)), Check: checkDataSourceStateMatchesResourceStateWithIgnores( "data.google_dns_managed_zone.qa", "google_dns_managed_zone.foo", map[string]struct{}{ - "dnssec_config.#": {}, - "private_visibility_config.#": {}, - "peering_config.#": {}, - "forwarding_config.#": {}, - "force_destroy": {}, - "labels.#": {}, - "creation_time": {}, + "dnssec_config.#": {}, + "private_visibility_config.#": {}, + "peering_config.#": {}, + "forwarding_config.#": {}, + "force_destroy": {}, + "labels.#": {}, + "creation_time": {}, + "cloud_logging_config.#": {}, + "cloud_logging_config.0.%": {}, + "cloud_logging_config.0.enable_logging": {}, <% unless version == "ga" -%> - "reverse_lookup": {}, + "reverse_lookup": {}, <% end -%> }, ), @@ -52,3 +80,38 @@ data "google_dns_managed_zone" "qa" { } `, managedZoneName) } + +// testAccCheckDNSManagedZoneDestroyProducerFramework is the framework version of the generated testAccCheckDNSManagedZoneDestroyProducer +// when we automate this, we'll use the automated version and can get rid of this +func testAccCheckDNSManagedZoneDestroyProducerFramework(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_dns_managed_zone" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + p := getTestFwProvider(t) + + url, err := replaceVarsForFrameworkTest(&p.ProdProvider, rs, "{{DNSBasePath}}projects/{{project}}/managedZones/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if !p.ProdProvider.billingProject.IsNull() && p.ProdProvider.billingProject.String() != "" { + billingProject = p.ProdProvider.billingProject.String() + } + + _, diags := sendFrameworkRequest(&p.ProdProvider, "GET", billingProject, url, p.ProdProvider.userAgent, nil) + if !diags.HasError() { + return fmt.Errorf("DNSManagedZone still exists at %s", url) + } + } + + return nil + } +} diff --git a/mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go b/mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go deleted file mode 100644 index 403f3ff013e1..000000000000 --- a/mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package google - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAcccDataSourceDnsRecordSet_basic(t *testing.T) { - t.Parallel() - - vcrTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDnsRecordSetDestroyProducer(t), - Steps: []resource.TestStep{ - { - Config: testAccDataSourceDnsRecordSet_basic(randString(t, 10), randString(t, 10)), - Check: resource.ComposeTestCheckFunc( - checkDataSourceStateMatchesResourceState("data.google_dns_record_set.rs", "google_dns_record_set.rs"), - ), - }, - }, - }) -} - -func testAccDataSourceDnsRecordSet_basic(zoneName, recordSetName string) string { - return fmt.Sprintf(` -resource "google_dns_managed_zone" "zone" { - name = "test-zone" - dns_name = "%s.hashicorptest.com." -} - -resource "google_dns_record_set" "rs" { - managed_zone = google_dns_managed_zone.zone.name - name = "%s.${google_dns_managed_zone.zone.dns_name}" - type = "A" - ttl = 300 - rrdatas = [ - "192.168.1.0", - ] -} - -data "google_dns_record_set" "rs" { - managed_zone = google_dns_record_set.rs.managed_zone - name = google_dns_record_set.rs.name - type = google_dns_record_set.rs.type -} -`, zoneName, recordSetName) -} diff --git a/mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go.erb b/mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go.erb new file mode 100644 index 000000000000..c2ff3b9d9c21 --- /dev/null +++ b/mmv1/third_party/terraform/tests/data_source_dns_record_set_test.go.erb @@ -0,0 +1,103 @@ +<% autogen_exception -%> +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataSourceDnsRecordSet_basic(t *testing.T) { + t.Parallel() + + var ttl1, ttl2 string // ttl is a computed string-type attribute that is easy to compare in the test + + managedZoneName := fmt.Sprintf("tf-test-zone-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckDnsRecordSetDestroyProducerFramework(t), + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion450(), + Config: testAccDataSourceDnsRecordSet_basic(managedZoneName, randString(t, 10), randString(t, 10)), + Check: resource.ComposeTestCheckFunc( + checkDataSourceStateMatchesResourceState("data.google_dns_record_set.rs", "google_dns_record_set.rs"), + testExtractResourceAttr("data.google_dns_record_set.rs", "ttl", &ttl1), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(t), + Config: testAccDataSourceDnsRecordSet_basic(managedZoneName, randString(t, 10), randString(t, 10)), + Check: resource.ComposeTestCheckFunc( + checkDataSourceStateMatchesResourceState("data.google_dns_record_set.rs", "google_dns_record_set.rs"), + testExtractResourceAttr("data.google_dns_record_set.rs", "ttl", &ttl2), + testCheckAttributeValuesEqual(&ttl1, &ttl2), + ), + }, + }, + }) +} + +func testAccDataSourceDnsRecordSet_basic(managedZoneName, zoneName, recordSetName string) string { + return fmt.Sprintf(` +resource "google_dns_managed_zone" "zone" { + name = "%s" + dns_name = "%s.hashicorptest.com." +} + +resource "google_dns_record_set" "rs" { + managed_zone = google_dns_managed_zone.zone.name + name = "%s.${google_dns_managed_zone.zone.dns_name}" + type = "A" + ttl = 300 + rrdatas = [ + "192.168.1.0", + ] +} + +data "google_dns_record_set" "rs" { + managed_zone = google_dns_record_set.rs.managed_zone + name = google_dns_record_set.rs.name + type = google_dns_record_set.rs.type +} +`, managedZoneName, zoneName, recordSetName) +} + +// testAccCheckDnsRecordSetDestroyProducerFramework is the framework version of the generated testAccCheckDnsRecordSetDestroyProducer +func testAccCheckDnsRecordSetDestroyProducerFramework(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_dns_record_set" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + p := getTestFwProvider(t) + + url, err := replaceVarsForFrameworkTest(&p.ProdProvider, rs, "{{DNSBasePath}}projects/{{project}}/managedZones/{{managed_zone}}/rrsets/{{name}}/{{type}}") + if err != nil { + return err + } + + billingProject := "" + + if !p.ProdProvider.billingProject.IsNull() && p.ProdProvider.billingProject.String() != "" { + billingProject = p.ProdProvider.billingProject.String() + } + + _, diags := sendFrameworkRequest(&p.ProdProvider, "GET", billingProject, url, p.ProdProvider.userAgent, nil) + if !diags.HasError() { + return fmt.Errorf("DNSResourceDnsRecordSet still exists at %s", url) + } + } + + return nil + } +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/utils/config.go.erb b/mmv1/third_party/terraform/utils/config.go.erb index 23866931bbea..72362bee78d1 100644 --- a/mmv1/third_party/terraform/utils/config.go.erb +++ b/mmv1/third_party/terraform/utils/config.go.erb @@ -16,6 +16,7 @@ import ( grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/sirupsen/logrus" "google.golang.org/api/option" @@ -264,6 +265,140 @@ var DefaultClientScopes = []string{ "https://www.googleapis.com/auth/userinfo.email", } +func HandleSDKDefaults(d *schema.ResourceData) { + if d.Get("impersonate_service_account") == "" { + d.Set("impersonate_service_account", multiEnvSearch([]string{ + "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", + })) + } + + if d.Get("project") == "" { + d.Set("project", multiEnvSearch([]string{ + "GOOGLE_PROJECT", + "GOOGLE_CLOUD_PROJECT", + "GCLOUD_PROJECT", + "CLOUDSDK_CORE_PROJECT", + })) + } + + if d.Get("billing_project") == "" { + d.Set("billing_project", multiEnvSearch([]string{ + "GOOGLE_BILLING_PROJECT", + })) + } + + if d.Get("region") == "" { + d.Set("region", multiEnvSearch([]string{ + "GOOGLE_REGION", + "GCLOUD_REGION", + "CLOUDSDK_COMPUTE_REGION", + })) + } + + if d.Get("zone") == "" { + d.Set("zone", multiEnvSearch([]string{ + "GOOGLE_ZONE", + "GCLOUD_ZONE", + "CLOUDSDK_COMPUTE_ZONE", + })) + } + + if d.Get("user_project_override") == "" { + d.Set("user_project_override", multiEnvSearch([]string{ + "USER_PROJECT_OVERRIDE", + })) + } + + if d.Get("request_reason") == "" { + d.Set("request_reason", multiEnvSearch([]string{ + "CLOUDSDK_CORE_REQUEST_REASON", + })) + } + + // Generated Products + <% products.each do |product| -%> + if d.Get("<%= product[:definitions].name.underscore -%>_custom_endpoint") == "" { + d.Set("<%= product[:definitions].name.underscore -%>_custom_endpoint", MultiEnvDefault([]string{ + "GOOGLE_<%= product[:definitions].name.underscore.upcase -%>_CUSTOM_ENDPOINT", + }, DefaultBasePaths[<%= product[:definitions].name -%>BasePathKey])) + } + <% end -%> + + if d.Get(CloudBillingCustomEndpointEntryKey) == "" { + d.Set(CloudBillingCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_CLOUD_BILLING_CUSTOM_ENDPOINT", + }, DefaultBasePaths[CloudBillingBasePathKey])) + } + + if d.Get(ComposerCustomEndpointEntryKey) == "" { + d.Set(ComposerCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_COMPOSER_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ComposerBasePathKey])) + } + + if d.Get(ContainerCustomEndpointEntryKey) == "" { + d.Set(ContainerCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_CONTAINER_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ContainerBasePathKey])) + } + + if d.Get(DataflowCustomEndpointEntryKey) == "" { + d.Set(DataflowCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_DATAFLOW_CUSTOM_ENDPOINT", + }, DefaultBasePaths[DataflowBasePathKey])) + } + + if d.Get(IamCredentialsCustomEndpointEntryKey) == "" { + d.Set(IamCredentialsCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_IAM_CREDENTIALS_CUSTOM_ENDPOINT", + }, DefaultBasePaths[IamCredentialsBasePathKey])) + } + + if d.Get(ResourceManagerV3CustomEndpointEntryKey) == "" { + d.Set(ResourceManagerV3CustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_RESOURCE_MANAGER_V3_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ResourceManagerV3BasePathKey])) + } + + <% unless version == "ga" -%> + if d.Get(RuntimeConfigCustomEndpointEntryKey) == "" { + d.Set(RuntimeConfigCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_RUNTIMECONFIG_CUSTOM_ENDPOINT", + }, DefaultBasePaths[RuntimeConfigBasePathKey])) + } + <% end -%> + + if d.Get(IAMCustomEndpointEntryKey) == "" { + d.Set(IAMCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_IAM_CUSTOM_ENDPOINT", + }, DefaultBasePaths[IAMBasePathKey])) + } + + if d.Get(ServiceNetworkingCustomEndpointEntryKey) == "" { + d.Set(ServiceNetworkingCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_SERVICE_NETWORKING_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ServiceNetworkingBasePathKey])) + } + + if d.Get(TagsLocationCustomEndpointEntryKey) == "" { + d.Set(TagsLocationCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_TAGS_LOCATION_CUSTOM_ENDPOINT", + }, DefaultBasePaths[TagsLocationBasePathKey])) + } + + if d.Get(ContainerAwsCustomEndpointEntryKey) == "" { + d.Set(ContainerAwsCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_CONTAINERAWS_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ContainerAwsBasePathKey])) + } + + if d.Get(ContainerAzureCustomEndpointEntryKey) == "" { + d.Set(ContainerAzureCustomEndpointEntryKey, MultiEnvDefault([]string{ + "GOOGLE_CONTAINERAZURE_CUSTOM_ENDPOINT", + }, DefaultBasePaths[ContainerAzureBasePathKey])) + } +} + func (c *Config) LoadAndValidate(ctx context.Context) error { if len(c.Scopes) == 0 { c.Scopes = DefaultClientScopes diff --git a/mmv1/third_party/terraform/utils/framework_provider.go.erb b/mmv1/third_party/terraform/utils/framework_provider.go.erb new file mode 100644 index 000000000000..6d938cb09efe --- /dev/null +++ b/mmv1/third_party/terraform/utils/framework_provider.go.erb @@ -0,0 +1,276 @@ +<% autogen_exception -%> +package google + +import ( + "context" + "net/http" + "time" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + + "google.golang.org/api/option" +) + +// Ensure the implementation satisfies the expected interfaces +var ( + _ provider.ProviderWithMetaSchema = &frameworkProvider{} + + defaultClientScopes = []string{ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + } +) + +// New is a helper function to simplify provider server and testing implementation. +func New(version string) provider.ProviderWithMetaSchema { + return &frameworkProvider{ + version: version, + } +} + +// frameworkProvider is the provider implementation. +type frameworkProvider struct{ + billingProject types.String + client *http.Client + context context.Context + gRPCLoggingOptions []option.ClientOption + pollInterval time.Duration + project types.String + region string + requestBatcherIam *RequestBatcher + requestBatcherServiceUsage *RequestBatcher + scopes []string + userAgent string + userProjectOverride bool + version string + + // paths for client setup + <% products.each do |product| -%> + <%= product[:definitions].name -%>BasePath string + <% end -%> +} + +// Metadata returns the provider type name. +func (p *frameworkProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "google" + resp.Version = p.version +} + +// MetaSchema returns the provider meta schema. +func (p *frameworkProvider) MetaSchema(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "module_name": metaschema.StringAttribute{ + Optional: true, + }, + }, + } +} + +// Schema defines the provider-level schema for configuration data. +func (p *frameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "credentials": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("access_token"), + }...), + CredentialsValidator(), + }, + }, + "access_token": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("credentials"), + }...), + }, + }, + "impersonate_service_account": schema.StringAttribute{ + Optional: true, + }, + "impersonate_service_account_delegates": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + "project": schema.StringAttribute{ + Optional: true, + }, + "billing_project": schema.StringAttribute{ + Optional: true, + }, + "region": schema.StringAttribute{ + Optional: true, + }, + "zone": schema.StringAttribute{ + Optional: true, + }, + "scopes": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + "user_project_override": schema.BoolAttribute{ + Optional: true, + }, + "request_timeout": schema.StringAttribute{ + Optional: true, + }, + "request_reason": schema.StringAttribute{ + Optional: true, + }, + + // Generated Products + <% products.each do |product| -%> + "<%= product[:definitions].name.underscore -%>_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + <% end -%> + + // Handwritten Products / Versioned / Atypical Entries + "cloud_billing_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "composer_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "container_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "dataflow_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "iam_credentials_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "resource_manager_v3_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + <% unless version == "ga" -%> + "runtimeconfig_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + <% end -%> + "iam_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "service_networking_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "tags_location_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + + // dcl + "container_aws_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + "container_azure_custom_endpoint": &schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "batching": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "send_after": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + NonNegativeDurationValidator(), + }, + }, + "enable_batching": schema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + }, + } + + configureDCLFrameworkProvider(&resp.Schema) +} + +// Configure prepares an API client for data sources and resources. +func (p *frameworkProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + var data ProviderModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Configuration values are now available. + p.ConfigureWithData(ctx, data, req.TerraformVersion, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Example client configuration for data sources and resources + resp.DataSourceData = p + resp.ResourceData = p +} + + +// DataSources defines the data sources implemented in the provider. +func (p *frameworkProvider) DataSources(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + NewGoogleDnsManagedZoneDataSource, + NewGoogleDnsRecordSetDataSource, + NewGoogleDnsKeysDataSource, + } +} + +// Resources defines the resources implemented in the provider. +func (p *frameworkProvider) Resources(_ context.Context) []func() resource.Resource { + return nil +} diff --git a/mmv1/third_party/terraform/utils/framework_provider_clients.go b/mmv1/third_party/terraform/utils/framework_provider_clients.go new file mode 100644 index 000000000000..80941eb1c6d7 --- /dev/null +++ b/mmv1/third_party/terraform/utils/framework_provider_clients.go @@ -0,0 +1,34 @@ +package google + +import ( + "fmt" + "strings" + + "google.golang.org/api/dns/v1" + "google.golang.org/api/option" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Methods to create new services from config +// Some base paths below need the version and possibly more of the path +// set on them. The client libraries are inconsistent about which values they need; +// while most only want the host URL, some older ones also want the version and some +// of those "projects" as well. You can find out if this is required by looking at +// the basePath value in the client library file. + +func (p *frameworkProvider) NewDnsClient(userAgent string, diags *diag.Diagnostics) *dns.Service { + dnsClientBasePath := removeBasePathVersion(p.DNSBasePath) + dnsClientBasePath = strings.ReplaceAll(dnsClientBasePath, "/dns/", "") + tflog.Info(p.context, fmt.Sprintf("Instantiating Google Cloud DNS client for path %s", dnsClientBasePath)) + clientDns, err := dns.NewService(p.context, option.WithHTTPClient(p.client)) + if err != nil { + diags.AddWarning("error creating client dns", err.Error()) + return nil + } + clientDns.UserAgent = userAgent + clientDns.BasePath = dnsClientBasePath + + return clientDns +} diff --git a/mmv1/third_party/terraform/utils/provider.go.erb b/mmv1/third_party/terraform/utils/provider.go.erb index bd155a519289..31b24d031123 100644 --- a/mmv1/third_party/terraform/utils/provider.go.erb +++ b/mmv1/third_party/terraform/utils/provider.go.erb @@ -51,9 +51,6 @@ func Provider() *schema.Provider { "impersonate_service_account": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", - }, nil), }, "impersonate_service_account_delegates": { @@ -62,46 +59,27 @@ func Provider() *schema.Provider { Elem: &schema.Schema{Type: schema.TypeString}, }, - "project": &schema.Schema{ + "project": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_PROJECT", - "GOOGLE_CLOUD_PROJECT", - "GCLOUD_PROJECT", - "CLOUDSDK_CORE_PROJECT", - }, nil), }, - "billing_project": &schema.Schema{ + "billing_project": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_BILLING_PROJECT", - }, nil), }, - "region": &schema.Schema{ + "region": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_REGION", - "GCLOUD_REGION", - "CLOUDSDK_COMPUTE_REGION", - }, nil), }, - "zone": &schema.Schema{ + "zone": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_ZONE", - "GCLOUD_ZONE", - "CLOUDSDK_COMPUTE_ZONE", - }, nil), }, - "scopes": &schema.Schema{ + "scopes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, @@ -116,13 +94,11 @@ func Provider() *schema.Provider { "send_after": { Type: schema.TypeString, Optional: true, - Default: "10s", ValidateFunc: validateNonNegativeDuration(), }, "enable_batching": { Type: schema.TypeBool, Optional: true, - Default: true, }, }, }, @@ -131,9 +107,6 @@ func Provider() *schema.Provider { "user_project_override": { Type: schema.TypeBool, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "USER_PROJECT_OVERRIDE", - }, nil), }, "request_timeout": { @@ -144,20 +117,14 @@ func Provider() *schema.Provider { "request_reason": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "CLOUDSDK_CORE_REQUEST_REASON", - }, nil), }, // Generated Products <% products.each do |product| -%> - "<%= product[:definitions].name.underscore -%>_custom_endpoint": &schema.Schema{ + "<%= product[:definitions].name.underscore -%>_custom_endpoint": { Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_<%= product[:definitions].name.underscore.upcase -%>_CUSTOM_ENDPOINT", - }, DefaultBasePaths[<%= product[:definitions].name -%>BasePathKey]), }, <% end -%> @@ -173,8 +140,6 @@ func Provider() *schema.Provider { <% end -%> IAMCustomEndpointEntryKey: IAMCustomEndpointEntry, ServiceNetworkingCustomEndpointEntryKey: ServiceNetworkingCustomEndpointEntry, - ServiceUsageCustomEndpointEntryKey: ServiceUsageCustomEndpointEntry, - BigtableAdminCustomEndpointEntryKey: BigtableAdminCustomEndpointEntry, TagsLocationCustomEndpointEntryKey: TagsLocationCustomEndpointEntry, // dcl @@ -564,6 +529,9 @@ end # products.each do } func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Provider) (interface{}, diag.Diagnostics) { + HandleSDKDefaults(d) + HandleDCLProviderDefaults(d) + config := Config{ Project: d.Get("project").(string), Region: d.Get("region").(string), diff --git a/mmv1/third_party/terraform/utils/provider_handwritten_endpoint.go.erb b/mmv1/third_party/terraform/utils/provider_handwritten_endpoint.go.erb index 68679685632f..b9fa035757a7 100644 --- a/mmv1/third_party/terraform/utils/provider_handwritten_endpoint.go.erb +++ b/mmv1/third_party/terraform/utils/provider_handwritten_endpoint.go.erb @@ -14,9 +14,6 @@ var CloudBillingCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_CLOUD_BILLING_CUSTOM_ENDPOINT", - }, DefaultBasePaths[CloudBillingBasePathKey]), } var ComposerCustomEndpointEntryKey = "composer_custom_endpoint" @@ -24,9 +21,6 @@ var ComposerCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_COMPOSER_CUSTOM_ENDPOINT", - }, DefaultBasePaths[ComposerBasePathKey]), } var ContainerCustomEndpointEntryKey = "container_custom_endpoint" @@ -34,9 +28,6 @@ var ContainerCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_CONTAINER_CUSTOM_ENDPOINT", - }, DefaultBasePaths[ContainerBasePathKey]), } var DataflowCustomEndpointEntryKey = "dataflow_custom_endpoint" @@ -44,9 +35,6 @@ var DataflowCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_DATAFLOW_CUSTOM_ENDPOINT", - }, DefaultBasePaths[DataflowBasePathKey]), } var IAMCustomEndpointEntryKey = "iam_custom_endpoint" @@ -54,9 +42,6 @@ var IAMCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_IAM_CUSTOM_ENDPOINT", - }, DefaultBasePaths[IAMBasePathKey]), } var IamCredentialsCustomEndpointEntryKey = "iam_credentials_custom_endpoint" @@ -64,9 +49,6 @@ var IamCredentialsCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_IAM_CREDENTIALS_CUSTOM_ENDPOINT", - }, DefaultBasePaths[IamCredentialsBasePathKey]), } var ResourceManagerV3CustomEndpointEntryKey = "resource_manager_v3_custom_endpoint" @@ -74,9 +56,6 @@ var ResourceManagerV3CustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_RESOURCE_MANAGER_V3_CUSTOM_ENDPOINT", - }, DefaultBasePaths[ResourceManagerV3BasePathKey]), } <% unless version == 'ga' -%> @@ -85,9 +64,6 @@ var RuntimeConfigCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_RUNTIMECONFIG_CUSTOM_ENDPOINT", - }, DefaultBasePaths[RuntimeConfigBasePathKey]), } <% end -%> @@ -96,9 +72,6 @@ var ServiceNetworkingCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_SERVICE_NETWORKING_CUSTOM_ENDPOINT", - }, DefaultBasePaths[ServiceNetworkingBasePathKey]), } var ServiceUsageCustomEndpointEntryKey = "service_usage_custom_endpoint" @@ -136,9 +109,6 @@ var ContainerAwsCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_CONTAINERAWS_CUSTOM_ENDPOINT", - }, DefaultBasePaths[ContainerAwsBasePathKey]), } var ContainerAzureCustomEndpointEntryKey = "container_azure_custom_endpoint" @@ -146,9 +116,6 @@ var ContainerAzureCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_CONTAINERAZURE_CUSTOM_ENDPOINT", - }, DefaultBasePaths[ContainerAzureBasePathKey]), } var TagsLocationCustomEndpointEntryKey = "tags_location_custom_endpoint" @@ -156,9 +123,6 @@ var TagsLocationCustomEndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validateCustomEndpoint, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_TAGS_LOCATION_CUSTOM_ENDPOINT", - }, DefaultBasePaths[TagsLocationBasePathKey]), } func validateCustomEndpoint(v interface{}, k string) (ws []string, errors []error) { diff --git a/mmv1/third_party/terraform/utils/provider_test.go.erb b/mmv1/third_party/terraform/utils/provider_test.go.erb index 40e5e3752ae2..31ed35995697 100644 --- a/mmv1/third_party/terraform/utils/provider_test.go.erb +++ b/mmv1/third_party/terraform/utils/provider_test.go.erb @@ -303,14 +303,17 @@ func getTestAccProviders(testName string, c resource.TestCase) map[string]*schem } var testProvider string providerMapKeys := reflect.ValueOf(c.Providers).MapKeys() - if strings.Contains(providerMapKeys[0].String(), "google-beta") { - testProvider = "google-beta" - } else { - testProvider = "google" - } - return map[string]*schema.Provider{ - testProvider: prov, + if len(providerMapKeys) > 0 { + if strings.Contains(providerMapKeys[0].String(), "google-beta") { + testProvider = "google-beta" + } else { + testProvider = "google" + } + return map[string]*schema.Provider{ + testProvider: prov, + } } + return map[string]*schema.Provider{} } func isVcrEnabled() bool { @@ -325,6 +328,8 @@ func vcrTest(t *testing.T, c resource.TestCase) { if isVcrEnabled() { providers := getTestAccProviders(t.Name(), c) c.Providers = providers + fwProviders := getTestAccFrameworkProviders(t.Name(), c) + c.ProtoV5ProviderFactories = fwProviders defer closeRecorder(t) } else if isReleaseDiffEnabled() { c = initializeReleaseDiffTest(c) diff --git a/mmv1/third_party/terraform/utils/transport_test.go b/mmv1/third_party/terraform/utils/transport_test.go index b0e188a62dd1..0f9aedf683e3 100644 --- a/mmv1/third_party/terraform/utils/transport_test.go +++ b/mmv1/third_party/terraform/utils/transport_test.go @@ -54,6 +54,51 @@ func replaceVarsForTest(config *Config, rs *terraform.ResourceState, linkTmpl st return re.ReplaceAllStringFunc(linkTmpl, replaceFunc), nil } +// This function isn't a test of transport.go; instead, it is used as an alternative +// to replaceVars inside tests. +func replaceVarsForFrameworkTest(prov *frameworkProvider, rs *terraform.ResourceState, linkTmpl string) (string, error) { + re := regexp.MustCompile("{{([[:word:]]+)}}") + var project, region, zone string + + if strings.Contains(linkTmpl, "{{project}}") { + project = rs.Primary.Attributes["project"] + } + + if strings.Contains(linkTmpl, "{{region}}") { + region = GetResourceNameFromSelfLink(rs.Primary.Attributes["region"]) + } + + if strings.Contains(linkTmpl, "{{zone}}") { + zone = GetResourceNameFromSelfLink(rs.Primary.Attributes["zone"]) + } + + replaceFunc := func(s string) string { + m := re.FindStringSubmatch(s)[1] + if m == "project" { + return project + } + if m == "region" { + return region + } + if m == "zone" { + return zone + } + + if v, ok := rs.Primary.Attributes[m]; ok { + return v + } + + // Attempt to draw values from the provider + if f := reflect.Indirect(reflect.ValueOf(prov)).FieldByName(m); f.IsValid() { + return f.String() + } + + return "" + } + + return re.ReplaceAllStringFunc(linkTmpl, replaceFunc), nil +} + func TestReplaceVars(t *testing.T) { cases := map[string]struct { Template string diff --git a/mmv1/third_party/terraform/utils/utils.go b/mmv1/third_party/terraform/utils/utils.go index f872179b438b..fdea32cce9db 100644 --- a/mmv1/third_party/terraform/utils/utils.go +++ b/mmv1/third_party/terraform/utils/utils.go @@ -596,3 +596,16 @@ func retryWhileIncompatibleOperation(timeout time.Duration, lockKey string, f fu return nil }) } + +// MultiEnvDefaultFunc is a helper function that returns the value of the first +// environment variable in the given list that returns a non-empty value. If +// none of the environment variables return a value, the default value is +// returned. +func MultiEnvDefault(ks []string, dv interface{}) interface{} { + for _, k := range ks { + if v := os.Getenv(k); v != "" { + return v + } + } + return dv +} diff --git a/tpgtools/templates/provider_dcl_endpoints.go.tmpl b/tpgtools/templates/provider_dcl_endpoints.go.tmpl index eb320239ce51..43b11d8e46f1 100644 --- a/tpgtools/templates/provider_dcl_endpoints.go.tmpl +++ b/tpgtools/templates/provider_dcl_endpoints.go.tmpl @@ -30,6 +30,8 @@ package google import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + framework_schema "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) // empty string is passed for dcl default since dcl @@ -40,9 +42,6 @@ var {{$pkg.BasePathIdentifier.ToTitle}}EndpointEntryKey = "{{$pkg.BasePathIdenti var {{$pkg.BasePathIdentifier.ToTitle}}EndpointEntry = &schema.Schema{ Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_{{$pkg.BasePathIdentifier.ToUpper}}_CUSTOM_ENDPOINT", - }, ""), } {{- end}} {{end}} @@ -63,6 +62,32 @@ func ConfigureDCLProvider(provider *schema.Provider) { {{- end}} } +func HandleDCLProviderDefaults(d *schema.ResourceData) { +{{- range $index, $pkg := . }} +{{- if $pkg.ShouldWriteProductBasePath }} + if d.Get({{$pkg.BasePathIdentifier.ToTitle}}EndpointEntryKey) == "" { + d.Set({{$pkg.BasePathIdentifier.ToTitle}}EndpointEntryKey, multiEnvSearch([]string{ + "GOOGLE_{{$pkg.BasePathIdentifier.ToUpper}}_CUSTOM_ENDPOINT", + })) + } +{{- end}} +{{- end}} +} + +// plugin-framework provider set-up +func configureDCLFrameworkProvider(frameworkSchema *framework_schema.Schema) { +{{- range $index, $pkg := . }} +{{- if $pkg.ShouldWriteProductBasePath }} + frameworkSchema.Attributes["{{$pkg.BasePathIdentifier}}_custom_endpoint"] = framework_schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + CustomEndpointValidator(), + }, + } +{{- end}} +{{- end}} +} + func ProviderDCLConfigure(d *schema.ResourceData, config *Config) interface{} { {{- range $index, $pkg := . }} {{- if $pkg.ShouldWriteProductBasePath }}