From 35cebede11bf3f6e78967d0743cbe5e295db91f0 Mon Sep 17 00:00:00 2001 From: Nick Elliot Date: Tue, 8 Apr 2025 09:41:53 -0700 Subject: [PATCH 1/8] HTTP transport functions for fwprovider (#13496) --- .../terraform/fwresource/field_helpers.go | 16 +- .../terraform/fwtransport/framework_utils.go | 317 ++++++++++++++++++ 2 files changed, 329 insertions(+), 4 deletions(-) diff --git a/mmv1/third_party/terraform/fwresource/field_helpers.go b/mmv1/third_party/terraform/fwresource/field_helpers.go index 54788d8346e7..a3b585f37225 100644 --- a/mmv1/third_party/terraform/fwresource/field_helpers.go +++ b/mmv1/third_party/terraform/fwresource/field_helpers.go @@ -17,10 +17,18 @@ import ( // 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 getProjectFromFrameworkSchema("project", rVal, pVal, diags) + return getProviderDefaultFromFrameworkSchema("project", rVal, pVal, diags) } -func getProjectFromFrameworkSchema(projectSchemaField string, rVal, pVal types.String, diags *diag.Diagnostics) types.String { +func GetRegionFramework(rVal, pVal types.String, diags *diag.Diagnostics) types.String { + return getProviderDefaultFromFrameworkSchema("region", rVal, pVal, diags) +} + +func GetZoneFramework(rVal, pVal types.String, diags *diag.Diagnostics) types.String { + return getProviderDefaultFromFrameworkSchema("zone", rVal, pVal, diags) +} + +func getProviderDefaultFromFrameworkSchema(schemaField string, rVal, pVal types.String, diags *diag.Diagnostics) types.String { if !rVal.IsNull() && rVal.ValueString() != "" { return rVal } @@ -29,7 +37,7 @@ func getProjectFromFrameworkSchema(projectSchemaField string, rVal, pVal types.S return pVal } - diags.AddError("required field is not set", fmt.Sprintf("%s is not set", projectSchemaField)) + diags.AddError("required field is not set", fmt.Sprintf("%s is not set", schemaField)) return types.String{} } @@ -54,7 +62,7 @@ func ParseProjectFieldValueFramework(resourceType, fieldValue, projectSchemaFiel } } - project := getProjectFromFrameworkSchema(projectSchemaField, rVal, pVal, diags) + project := getProviderDefaultFromFrameworkSchema(projectSchemaField, rVal, pVal, diags) if diags.HasError() { return nil } diff --git a/mmv1/third_party/terraform/fwtransport/framework_utils.go b/mmv1/third_party/terraform/fwtransport/framework_utils.go index b297b475cb25..4ca7ab3dc0c6 100644 --- a/mmv1/third_party/terraform/fwtransport/framework_utils.go +++ b/mmv1/third_party/terraform/fwtransport/framework_utils.go @@ -1,17 +1,29 @@ package fwtransport import ( + "bytes" "context" + "encoding/json" "fmt" + "net/http" "os" + "reflect" + "regexp" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwresource" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "google.golang.org/api/googleapi" ) const uaEnvVar = "TF_APPEND_USER_AGENT" @@ -47,3 +59,308 @@ func HandleDatasourceNotFoundError(ctx context.Context, err error, state *tfsdk. diags.AddError(fmt.Sprintf("Error when reading or editing %s", resource), err.Error()) } + +var DefaultRequestTimeout = 5 * time.Minute + +type SendRequestOptions struct { + Config *transport_tpg.Config + Method string + Project string + RawURL string + UserAgent string + Body map[string]any + Timeout time.Duration + Headers http.Header + ErrorRetryPredicates []transport_tpg.RetryErrorPredicateFunc + ErrorAbortPredicates []transport_tpg.RetryErrorPredicateFunc +} + +func SendRequest(opt SendRequestOptions, diags *diag.Diagnostics) map[string]interface{} { + reqHeaders := opt.Headers + if reqHeaders == nil { + reqHeaders = make(http.Header) + } + reqHeaders.Set("User-Agent", opt.UserAgent) + reqHeaders.Set("Content-Type", "application/json") + + if opt.Config.UserProjectOverride && opt.Project != "" { + // When opt.Project is "NO_BILLING_PROJECT_OVERRIDE" in the function GetCurrentUserEmail, + // set the header X-Goog-User-Project to be empty string. + if opt.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", opt.Project) + } + } + + if opt.Timeout == 0 { + opt.Timeout = DefaultRequestTimeout + } + + var res *http.Response + err := transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: func() error { + var buf bytes.Buffer + if opt.Body != nil { + err := json.NewEncoder(&buf).Encode(opt.Body) + if err != nil { + return err + } + } + + u, err := transport_tpg.AddQueryParams(opt.RawURL, map[string]string{"alt": "json"}) + if err != nil { + return err + } + req, err := http.NewRequest(opt.Method, u, &buf) + if err != nil { + return err + } + + req.Header = reqHeaders + res, err = opt.Config.Client.Do(req) + if err != nil { + return err + } + + if err := googleapi.CheckResponse(res); err != nil { + googleapi.CloseBody(res) + return err + } + + return nil + }, + Timeout: opt.Timeout, + ErrorRetryPredicates: opt.ErrorRetryPredicates, + ErrorAbortPredicates: opt.ErrorAbortPredicates, + }) + if err != nil { + diags.AddError("Error when sending HTTP request: ", err.Error()) + return nil + } + + 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 + } + + // 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 + } + result := make(map[string]interface{}) + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + diags.AddError("Error when sending HTTP request: ", err.Error()) + return nil + } + + return result +} + +type DefaultVars struct { + BillingProject types.String + Project types.String + Region types.String + Zone types.String +} + +func ReplaceVars(ctx context.Context, req interface{}, diags *diag.Diagnostics, data DefaultVars, config *transport_tpg.Config, linkTmpl string) string { + return ReplaceVarsRecursive(ctx, req, diags, data, config, linkTmpl, false, 0) +} + +// relaceVarsForId shortens variables by running them through GetResourceNameFromSelfLink +// this allows us to use long forms of variables from configs without needing +// custom id formats. For instance: +// accessPolicies/{{access_policy}}/accessLevels/{{access_level}} +// with values: +// access_policy: accessPolicies/foo +// access_level: accessPolicies/foo/accessLevels/bar +// becomes accessPolicies/foo/accessLevels/bar +func ReplaceVarsForId(ctx context.Context, req interface{}, diags *diag.Diagnostics, data DefaultVars, config *transport_tpg.Config, linkTmpl string) string { + return ReplaceVarsRecursive(ctx, req, diags, data, config, linkTmpl, true, 0) +} + +// ReplaceVars must be done recursively because there are baseUrls that can contain references to regions +// (eg cloudrun service) there aren't any cases known for 2+ recursion but we will track a run away +// substitution as 10+ calls to allow for future use cases. +func ReplaceVarsRecursive(ctx context.Context, req interface{}, diags *diag.Diagnostics, data DefaultVars, config *transport_tpg.Config, linkTmpl string, shorten bool, depth int) string { + if depth > 10 { + diags.AddError("url building error", "Recursive substitution detected.") + } + + // https://github.com/google/re2/wiki/Syntax + re := regexp.MustCompile("{{([%[:word:]]+)}}") + f := BuildReplacementFunc(ctx, re, req, diags, data, config, linkTmpl, shorten) + if diags.HasError() { + return "" + } + final := re.ReplaceAllStringFunc(linkTmpl, f) + + if re.Match([]byte(final)) { + return ReplaceVarsRecursive(ctx, req, diags, data, config, final, shorten, depth+1) + } + + return final +} + +// This function replaces references to Terraform properties (in the form of {{var}}) with their value in Terraform +// It also replaces {{project}}, {{project_id_or_project}}, {{region}}, and {{zone}} with their appropriate values +// This function supports URL-encoding the result by prepending '%' to the field name e.g. {{%var}} +func BuildReplacementFunc(ctx context.Context, re *regexp.Regexp, req interface{}, diags *diag.Diagnostics, data DefaultVars, config *transport_tpg.Config, linkTmpl string, shorten bool) func(string) string { + var project, region, zone string + var projectID types.String + + if strings.Contains(linkTmpl, "{{project}}") { + project = fwresource.GetProjectFramework(data.Project, types.StringValue(config.Project), diags).ValueString() + if diags.HasError() { + return nil + } + if shorten { + project = strings.TrimPrefix(project, "projects/") + } + } + + if strings.Contains(linkTmpl, "{{project_id_or_project}}") { + var diagInfo diag.Diagnostics + switch req.(type) { + case resource.CreateRequest: + pReq := req.(resource.CreateRequest) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("project_id"), &projectID) + case resource.UpdateRequest: + pReq := req.(resource.UpdateRequest) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("project_id"), &projectID) + case resource.ReadRequest: + sReq := req.(resource.ReadRequest) + diagInfo = sReq.State.GetAttribute(ctx, path.Root("project_id"), &projectID) + case resource.DeleteRequest: + sReq := req.(resource.DeleteRequest) + diagInfo = sReq.State.GetAttribute(ctx, path.Root("project_id"), &projectID) + } + diags.Append(diagInfo...) + if diags.HasError() { + return nil + } + if projectID.ValueString() != "" { + project = fwresource.GetProjectFramework(data.Project, types.StringValue(config.Project), diags).ValueString() + if diags.HasError() { + return nil + } + } + if shorten { + project = strings.TrimPrefix(project, "projects/") + projectID = types.StringValue(strings.TrimPrefix(projectID.ValueString(), "projects/")) + } + } + + if strings.Contains(linkTmpl, "{{region}}") { + region = fwresource.GetRegionFramework(data.Region, types.StringValue(config.Region), diags).ValueString() + if diags.HasError() { + return nil + } + if shorten { + region = strings.TrimPrefix(region, "regions/") + } + } + + if strings.Contains(linkTmpl, "{{zone}}") { + zone = fwresource.GetRegionFramework(data.Zone, types.StringValue(config.Zone), diags).ValueString() + if diags.HasError() { + return nil + } + if shorten { + zone = strings.TrimPrefix(region, "zones/") + } + } + + f := func(s string) string { + + m := re.FindStringSubmatch(s)[1] + if m == "project" { + return project + } + if m == "project_id_or_project" { + if projectID.ValueString() != "" { + return projectID.ValueString() + } + return project + } + if m == "region" { + return region + } + if m == "zone" { + return zone + } + if string(m[0]) == "%" { + var v types.String + var diagInfo diag.Diagnostics + switch req.(type) { + case resource.CreateRequest: + pReq := req.(resource.CreateRequest) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m[1:]"), &v) + case resource.UpdateRequest: + pReq := req.(resource.UpdateRequest) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m[1:]"), &v) + case resource.ReadRequest: + sReq := req.(resource.ReadRequest) + diagInfo = sReq.State.GetAttribute(ctx, path.Root("m[1:]"), &v) + case resource.DeleteRequest: + sReq := req.(resource.DeleteRequest) + diagInfo = sReq.State.GetAttribute(ctx, path.Root("m[1:]"), &v) + } + diags.Append(diagInfo...) + if !diags.HasError() { + if v.ValueString() != "" { + if shorten { + return tpgresource.GetResourceNameFromSelfLink(fmt.Sprintf("%v", v.ValueString())) + } else { + return fmt.Sprintf("%v", v.ValueString()) + } + } + } + } else { + var v types.String + var diagInfo diag.Diagnostics + switch req.(type) { + case resource.CreateRequest: + pReq := req.(resource.CreateRequest) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m"), &v) + case resource.UpdateRequest: + pReq := req.(resource.UpdateRequest) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m"), &v) + case resource.ReadRequest: + sReq := req.(resource.ReadRequest) + diagInfo = sReq.State.GetAttribute(ctx, path.Root("m"), &v) + case resource.DeleteRequest: + sReq := req.(resource.DeleteRequest) + diagInfo = sReq.State.GetAttribute(ctx, path.Root("m"), &v) + } + diags.Append(diagInfo...) + if !diags.HasError() { + if v.ValueString() != "" { + if shorten { + return tpgresource.GetResourceNameFromSelfLink(fmt.Sprintf("%v", v.ValueString())) + } else { + return fmt.Sprintf("%v", v.ValueString()) + } + } + } + } + + // terraform-google-conversion doesn't provide a provider config in tests. + if config != nil { + // Attempt to draw values from the provider config if it's present. + if f := reflect.Indirect(reflect.ValueOf(config)).FieldByName(m); f.IsValid() { + return f.String() + } + } + return "" + } + + return f +} From a963ebbb47abb3bc32536aaa43413336ab26439b Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Wed, 16 Apr 2025 10:54:27 -0500 Subject: [PATCH 2/8] framework compute network datasource (#13567) Co-authored-by: Nick Elliot --- .../terraform/acctest/vcr_utils.go | 2 + .../fwprovider/framework_provider.go.tmpl | 1 + ...data_source_google_compute_network.go.tmpl | 187 ++++++++++++++++++ ...data_source_google_compute_network_test.go | 86 ++++++++ 4 files changed, 276 insertions(+) create mode 100644 mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network.go.tmpl create mode 100644 mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network_test.go diff --git a/mmv1/third_party/terraform/acctest/vcr_utils.go b/mmv1/third_party/terraform/acctest/vcr_utils.go index 5e55ca88cb2d..e4fd00c15339 100644 --- a/mmv1/third_party/terraform/acctest/vcr_utils.go +++ b/mmv1/third_party/terraform/acctest/vcr_utils.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/terraform-provider-google/google/fwprovider" tpgprovider "github.com/hashicorp/terraform-provider-google/google/provider" + "github.com/hashicorp/terraform-provider-google/google/services/compute" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" @@ -402,6 +403,7 @@ func (p *frameworkTestProvider) Configure(ctx context.Context, req provider.Conf func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() datasource.DataSource { ds := p.FrameworkProvider.DataSources(ctx) ds = append(ds, fwprovider.NewGoogleProviderConfigPluginFrameworkDataSource) // google_provider_config_plugin_framework + ds = append(ds, compute.NewComputeNetworkFWDataSource) // google_fw_compute_network return ds } diff --git a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl index 80c944261338..e71a6d220941 100644 --- a/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl +++ b/mmv1/third_party/terraform/fwprovider/framework_provider.go.tmpl @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-provider-google/google/functions" "github.com/hashicorp/terraform-provider-google/google/fwmodels" "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager" + "github.com/hashicorp/terraform-provider-google/version" {{- if ne $.TargetVersionName "ga" }} "github.com/hashicorp/terraform-provider-google/google/services/firebase" diff --git a/mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network.go.tmpl b/mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network.go.tmpl new file mode 100644 index 000000000000..8df53611aa17 --- /dev/null +++ b/mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network.go.tmpl @@ -0,0 +1,187 @@ +package compute + +import ( + "context" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + {{ if eq $.TargetVersionName `ga` }} + "google.golang.org/api/compute/v1" + {{- else }} + compute "google.golang.org/api/compute/v0.beta" + {{- end }} + + "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwresource" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &ComputeNetworkFWDataSource{} + _ datasource.DataSourceWithConfigure = &ComputeNetworkFWDataSource{} +) + +// NewComputeNetworkFWDataSource is a helper function to simplify the provider implementation. +func NewComputeNetworkFWDataSource() datasource.DataSource { + return &ComputeNetworkFWDataSource{} +} + +// ComputeNetworkFWDataSource is the data source implementation. +type ComputeNetworkFWDataSource struct { + client *compute.Service + providerConfig *transport_tpg.Config +} + +type ComputeNetworkModel struct { + Id types.String `tfsdk:"id"` + Project types.String `tfsdk:"project"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + NetworkId types.Int64 `tfsdk:"network_id"` + NumericId types.String `tfsdk:"numeric_id"` + GatewayIpv4 types.String `tfsdk:"gateway_ipv4"` + InternalIpv6Range types.String `tfsdk:"internal_ipv6_range"` + SelfLink types.String `tfsdk:"self_link"` + // NetworkProfile types.String `tfsdk:"network_profile"` + // SubnetworksSelfLinks types.List `tfsdk:"subnetworks_self_links"` +} + +// Metadata returns the data source type name. +func (d *ComputeNetworkFWDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_fw_compute_network" +} + +func (d *ComputeNetworkFWDataSource) 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.(*transport_tpg.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *transport_tpg.Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = p.NewComputeClient(p.UserAgent) + if resp.Diagnostics.HasError() { + return + } + d.providerConfig = p +} + +// Schema defines the schema for the data source. +func (d *ComputeNetworkFWDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A data source to get network details.", + + Attributes: map[string]schema.Attribute{ + "project": schema.StringAttribute{ + Description: `The project name.`, + MarkdownDescription: `The project name.`, + Optional: true, + }, + "name": schema.StringAttribute{ + Description: `The name of the Compute network.`, + MarkdownDescription: `The name of the Compute network.`, + Required: true, + }, + "description": schema.StringAttribute{ + Description: `The description of the network.`, + MarkdownDescription: `The description of the network.`, + Computed: true, + }, + "network_id": schema.Int64Attribute{ + Description: `The network ID.`, + MarkdownDescription: `The network ID.`, + Computed: true, + }, + "numeric_id": schema.StringAttribute{ + Description: `The numeric ID of the network. Deprecated in favor of network_id.`, + MarkdownDescription: `The numeric ID of the network. Deprecated in favor of network_id.`, + Computed: true, + DeprecationMessage: "`numeric_id` is deprecated and will be removed in a future major release. Use `network_id` instead.", + }, + "gateway_ipv4": schema.StringAttribute{ + Description: `The gateway address for default routing out of the network.`, + MarkdownDescription: `The gateway address for default routing out of the network.`, + Computed: true, + }, + "internal_ipv6_range": schema.StringAttribute{ + Description: `The internal ipv6 address range of the network.`, + MarkdownDescription: `The internal ipv6 address range of the network.`, + Computed: true, + }, + "self_link": schema.StringAttribute{ + Description: `The network self link.`, + MarkdownDescription: `The network self link.`, + Computed: true, + }, + // This is included for backwards compatibility with the original, SDK-implemented data source. + "id": schema.StringAttribute{ + Description: "Project identifier", + MarkdownDescription: "Project identifier", + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *ComputeNetworkFWDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data ComputeNetworkModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + d.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, d.client.UserAgent) + + project := fwresource.GetProjectFramework(data.Project, types.StringValue(d.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // GET Request + clientResp, err := d.client.Networks.Get(project.ValueString(), data.Name.ValueString()).Do() + if err != nil { + fwtransport.HandleDatasourceNotFoundError(ctx, err, &resp.State, fmt.Sprintf("dataSourceComputeNetwork %q", data.Name.ValueString()), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + } + + tflog.Trace(ctx, "read compute network data source") + + // Put data in model + id := fmt.Sprintf("projects/%s/global/networks/%s", project.ValueString(), clientResp.Name) + data.Id = types.StringValue(id) + data.Description = types.StringValue(clientResp.Description) + data.NetworkId = types.Int64Value(int64(clientResp.Id)) + data.NumericId = types.StringValue(strconv.Itoa(int(clientResp.Id))) + data.GatewayIpv4 = types.StringValue(clientResp.GatewayIPv4) + data.InternalIpv6Range = types.StringValue(clientResp.InternalIpv6Range) + data.SelfLink = types.StringValue(clientResp.SelfLink) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network_test.go b/mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network_test.go new file mode 100644 index 000000000000..ae82326c793d --- /dev/null +++ b/mmv1/third_party/terraform/services/compute/fw_data_source_google_compute_network_test.go @@ -0,0 +1,86 @@ +package compute_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" +) + +func TestAccDataSourceGoogleFWNetwork(t *testing.T) { + t.Parallel() + + networkName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceGoogleNetworkFWConfig(networkName), + Check: resource.ComposeTestCheckFunc( + testAccDataSourceGoogleFWNetworkCheck("data.google_fw_compute_network.my_network", "google_compute_network.foobar"), + ), + }, + }, + }) +} + +func testAccDataSourceGoogleFWNetworkCheck(data_source_name string, resource_name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ds, ok := s.RootModule().Resources[data_source_name] + if !ok { + return fmt.Errorf("root module has no resource called %s", data_source_name) + } + + rs, ok := s.RootModule().Resources[resource_name] + if !ok { + return fmt.Errorf("can't find %s in state", resource_name) + } + + ds_attr := ds.Primary.Attributes + rs_attr := rs.Primary.Attributes + network_attrs_to_test := []string{ + "id", + "name", + "network_id", + "numeric_id", + "description", + "internal_ipv6_range", + } + + for _, attr_to_check := range network_attrs_to_test { + if ds_attr[attr_to_check] != rs_attr[attr_to_check] { + return fmt.Errorf( + "%s is %s; want %s", + attr_to_check, + ds_attr[attr_to_check], + rs_attr[attr_to_check], + ) + } + } + + if !tpgresource.CompareSelfLinkOrResourceName("", ds_attr["self_link"], rs_attr["self_link"], nil) && ds_attr["self_link"] != rs_attr["self_link"] { + return fmt.Errorf("self link does not match: %s vs %s", ds_attr["self_link"], rs_attr["self_link"]) + } + + return nil + } +} + +func testAccDataSourceGoogleNetworkFWConfig(name string) string { + return fmt.Sprintf(` +resource "google_compute_network" "foobar" { + name = "%s" + description = "my-description" + enable_ula_internal_ipv6 = true + auto_create_subnetworks = false +} + +data "google_fw_compute_network" "my_network" { + name = google_compute_network.foobar.name +} +`, name) +} From b5d95299a9603f70c7c0aa275c659bb783e591e2 Mon Sep 17 00:00:00 2001 From: Nick Elliot Date: Mon, 21 Apr 2025 13:33:03 -0700 Subject: [PATCH 3/8] pubsub_lite_reservation demo fwprovider conversion (#13672) --- .../terraform/acctest/vcr_utils.go | 8 + .../terraform/fwtransport/framework_utils.go | 26 +- .../fw_resource_pubsub_lite_reservation.go | 383 ++++++++++++++++++ ...w_resource_pubsub_lite_reservation_test.go | 56 +++ 4 files changed, 461 insertions(+), 12 deletions(-) create mode 100644 mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation.go create mode 100644 mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation_test.go diff --git a/mmv1/third_party/terraform/acctest/vcr_utils.go b/mmv1/third_party/terraform/acctest/vcr_utils.go index e4fd00c15339..2107ca354320 100644 --- a/mmv1/third_party/terraform/acctest/vcr_utils.go +++ b/mmv1/third_party/terraform/acctest/vcr_utils.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform-provider-google/google/fwprovider" tpgprovider "github.com/hashicorp/terraform-provider-google/google/provider" "github.com/hashicorp/terraform-provider-google/google/services/compute" + "github.com/hashicorp/terraform-provider-google/google/services/pubsublite" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" @@ -33,6 +34,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" fwDiags "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/provider" + fwResource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -407,6 +409,12 @@ func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() dataso return ds } +func (p *frameworkTestProvider) Resources(ctx context.Context) []func() fwResource.Resource { + r := p.FrameworkProvider.Resources(ctx) + r = append(r, pubsublite.NewGooglePubsubLiteReservationFWResource) // google_fwprovider_pubsub_lite_reservation + return r +} + // GetSDKProvider gets the SDK provider for use in acceptance tests // If VCR is in use, the configure function is overwritten. // See usage in MuxedProviders diff --git a/mmv1/third_party/terraform/fwtransport/framework_utils.go b/mmv1/third_party/terraform/fwtransport/framework_utils.go index 4ca7ab3dc0c6..255e694596e6 100644 --- a/mmv1/third_party/terraform/fwtransport/framework_utils.go +++ b/mmv1/third_party/terraform/fwtransport/framework_utils.go @@ -302,19 +302,20 @@ func BuildReplacementFunc(ctx context.Context, re *regexp.Regexp, req interface{ switch req.(type) { case resource.CreateRequest: pReq := req.(resource.CreateRequest) - diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m[1:]"), &v) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root(m[1:]), &v) case resource.UpdateRequest: pReq := req.(resource.UpdateRequest) - diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m[1:]"), &v) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root(m[1:]), &v) case resource.ReadRequest: sReq := req.(resource.ReadRequest) - diagInfo = sReq.State.GetAttribute(ctx, path.Root("m[1:]"), &v) + diagInfo = sReq.State.GetAttribute(ctx, path.Root(m[1:]), &v) case resource.DeleteRequest: sReq := req.(resource.DeleteRequest) - diagInfo = sReq.State.GetAttribute(ctx, path.Root("m[1:]"), &v) + diagInfo = sReq.State.GetAttribute(ctx, path.Root(m[1:]), &v) } - diags.Append(diagInfo...) - if !diags.HasError() { + //an error here means the attribute was not found, we want to do nothing in that case + if !diagInfo.HasError() { + diags.Append(diagInfo...) if v.ValueString() != "" { if shorten { return tpgresource.GetResourceNameFromSelfLink(fmt.Sprintf("%v", v.ValueString())) @@ -329,19 +330,20 @@ func BuildReplacementFunc(ctx context.Context, re *regexp.Regexp, req interface{ switch req.(type) { case resource.CreateRequest: pReq := req.(resource.CreateRequest) - diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m"), &v) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root(m), &v) case resource.UpdateRequest: pReq := req.(resource.UpdateRequest) - diagInfo = pReq.Plan.GetAttribute(ctx, path.Root("m"), &v) + diagInfo = pReq.Plan.GetAttribute(ctx, path.Root(m), &v) case resource.ReadRequest: sReq := req.(resource.ReadRequest) - diagInfo = sReq.State.GetAttribute(ctx, path.Root("m"), &v) + diagInfo = sReq.State.GetAttribute(ctx, path.Root(m), &v) case resource.DeleteRequest: sReq := req.(resource.DeleteRequest) - diagInfo = sReq.State.GetAttribute(ctx, path.Root("m"), &v) + diagInfo = sReq.State.GetAttribute(ctx, path.Root(m), &v) } - diags.Append(diagInfo...) - if !diags.HasError() { + //an error here means the attribute was not found, we want to do nothing in that case + if !diagInfo.HasError() { + diags.Append(diagInfo...) if v.ValueString() != "" { if shorten { return tpgresource.GetResourceNameFromSelfLink(fmt.Sprintf("%v", v.ValueString())) diff --git a/mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation.go b/mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation.go new file mode 100644 index 000000000000..685428eca2de --- /dev/null +++ b/mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation.go @@ -0,0 +1,383 @@ +package pubsublite + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwresource" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "google.golang.org/api/pubsublite/v1" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &GooglePubsubLiteReservationFWResource{} + _ resource.ResourceWithConfigure = &GooglePubsubLiteReservationFWResource{} +) + +// NewGooglePubsubLiteReservationResource is a helper function to simplify the provider implementation. +func NewGooglePubsubLiteReservationFWResource() resource.Resource { + return &GooglePubsubLiteReservationFWResource{} +} + +// GooglePubsubLiteReservationResource is the resource implementation. +type GooglePubsubLiteReservationFWResource struct { + client *pubsublite.Service + providerConfig *transport_tpg.Config +} + +type GooglePubsubLiteReservationModel struct { + Id types.String `tfsdk:"id"` + Project types.String `tfsdk:"project"` + Region types.String `tfsdk:"region"` + Name types.String `tfsdk:"name"` + ThroughputCapacity types.Int64 `tfsdk:"throughput_capacity"` +} + +// Metadata returns the resource type name. +func (d *GooglePubsubLiteReservationFWResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_fwprovider_pubsub_lite_reservation" +} + +func (d *GooglePubsubLiteReservationFWResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + p, ok := req.ProviderData.(*transport_tpg.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *transport_tpg.Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.providerConfig = p +} + +// Schema defines the schema for the data source. +func (d *GooglePubsubLiteReservationFWResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Pubsub Lite Reservation resource description", + + Attributes: map[string]schema.Attribute{ + "project": schema.StringAttribute{ + Description: "The project id of the Pubsub Lite Reservation.", + MarkdownDescription: "The project id of the Pubsub Lite Reservation.", + Required: true, + }, + "region": schema.StringAttribute{ + Description: "The region of the Pubsub Lite Reservation.", + MarkdownDescription: "The region of the Pubsub Lite Reservation.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: `The display name of the project.`, + MarkdownDescription: `The display name of the project.`, + Required: true, + }, + "throughput_capacity": schema.Int64Attribute{ + Description: `The reserved throughput capacity. Every unit of throughput capacity is equivalent to 1 MiB/s of published messages or 2 MiB/s of subscribed messages.`, + MarkdownDescription: `The reserved throughput capacity. Every unit of throughput capacity is equivalent to 1 MiB/s of published messages or 2 MiB/s of subscribed messages.`, + Required: true, + }, + // This is included for backwards compatibility with the original, SDK-implemented data source. + "id": schema.StringAttribute{ + Description: "Project identifier", + MarkdownDescription: "Project identifier", + Computed: true, + }, + }, + } +} + +func (d *GooglePubsubLiteReservationFWResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GooglePubsubLiteReservationModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + userAgent := fwtransport.GenerateFrameworkUserAgentString(metaData, d.providerConfig.UserAgent) + + obj := make(map[string]interface{}) + + obj["throughputCapacity"] = data.ThroughputCapacity.ValueInt64() + + data.Project = fwresource.GetProjectFramework(data.Project, types.StringValue(d.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + data.Region = fwresource.GetRegionFramework(data.Region, types.StringValue(d.providerConfig.Region), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + billingProject := data.Project + + var schemaDefaultVals fwtransport.DefaultVars + schemaDefaultVals.Project = data.Project + schemaDefaultVals.Region = data.Region + + url := fwtransport.ReplaceVars(ctx, req, &resp.Diagnostics, schemaDefaultVals, d.providerConfig, "{{PubsubLiteBasePath}}projects/{{project}}/locations/{{region}}/reservations?reservationId={{name}}") + if resp.Diagnostics.HasError() { + return + } + tflog.Trace(ctx, fmt.Sprintf("[DEBUG] Creating new Reservation: %#v", obj)) + + headers := make(http.Header) + res := fwtransport.SendRequest(fwtransport.SendRequestOptions{ + Config: d.providerConfig, + Method: "POST", + Project: billingProject.ValueString(), + RawURL: url, + UserAgent: userAgent, + Body: obj, + Headers: headers, + }, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "create fwprovider google_pubsub_lite resource") + + // Put data in model + data.Id = types.StringValue(fmt.Sprintf("projects/%s/locations/%s/reservations/%s", data.Project.ValueString(), data.Region.ValueString(), data.Name.ValueString())) + data.ThroughputCapacity = types.Int64Value(res["throughputCapacity"].(int64)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// Read refreshes the Terraform state with the latest data. +func (d *GooglePubsubLiteReservationFWResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data GooglePubsubLiteReservationModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + userAgent := fwtransport.GenerateFrameworkUserAgentString(metaData, d.providerConfig.UserAgent) + + data.Project = fwresource.GetProjectFramework(data.Project, types.StringValue(d.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + data.Region = fwresource.GetRegionFramework(data.Region, types.StringValue(d.providerConfig.Region), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + billingProject := data.Project + + var schemaDefaultVals fwtransport.DefaultVars + schemaDefaultVals.Project = data.Project + schemaDefaultVals.Region = data.Region + + url := fwtransport.ReplaceVars(ctx, req, &resp.Diagnostics, schemaDefaultVals, d.providerConfig, "{{PubSubLiteBasePath}}projects/{{project}}/locations/{{region}}/instances/{{name}}") + + if resp.Diagnostics.HasError() { + return + } + + headers := make(http.Header) + res := fwtransport.SendRequest(fwtransport.SendRequestOptions{ + Config: d.providerConfig, + Method: "GET", + Project: billingProject.ValueString(), + RawURL: url, + UserAgent: userAgent, + Headers: headers, + }, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "read fwprovider google_pubsub_lite resource") + + // Put data in model + data.Id = types.StringValue(fmt.Sprintf("projects/%s/locations/%s/instances/%s", data.Project.ValueString(), data.Region.ValueString(), data.Name.ValueString())) + data.ThroughputCapacity = types.Int64Value(res["throughputCapacity"].(int64)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *GooglePubsubLiteReservationFWResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state GooglePubsubLiteReservationModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + userAgent := fwtransport.GenerateFrameworkUserAgentString(metaData, d.providerConfig.UserAgent) + + obj := make(map[string]interface{}) + + obj["throughputCapacity"] = plan.ThroughputCapacity.ValueInt64() + + plan.Project = fwresource.GetProjectFramework(plan.Project, types.StringValue(d.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + plan.Region = fwresource.GetRegionFramework(plan.Region, types.StringValue(d.providerConfig.Region), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + billingProject := plan.Project + + var schemaDefaultVals fwtransport.DefaultVars + schemaDefaultVals.Project = plan.Project + schemaDefaultVals.Region = plan.Region + + url := fwtransport.ReplaceVars(ctx, req, &resp.Diagnostics, schemaDefaultVals, d.providerConfig, "{{PubSubLiteBasePath}}projects/{{project}}/locations/{{region}}/instances/{{name}}") + + if resp.Diagnostics.HasError() { + return + } + tflog.Trace(ctx, fmt.Sprintf("[DEBUG] Updating Reservation: %#v", obj)) + + headers := make(http.Header) + + updateMask := []string{} + if !plan.ThroughputCapacity.Equal(state.ThroughputCapacity) { + updateMask = append(updateMask, "throughputCapacity") + } + + // updateMask is a URL parameter but not present in the schema, so ReplaceVars + // won't set it + var err error + url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + resp.Diagnostics.AddError("Error when sending HTTP request: ", err.Error()) + return + } + + res := fwtransport.SendRequest(fwtransport.SendRequestOptions{ + Config: d.providerConfig, + Method: "PATCH", + Project: billingProject.ValueString(), + RawURL: url, + UserAgent: userAgent, + Body: obj, + Headers: headers, + }, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "update fwprovider google_pubsub_lite resource") + + // Put data in model + plan.Id = types.StringValue(fmt.Sprintf("projects/%s/locations/%s/instances/%s", plan.Project.ValueString(), plan.Region.ValueString(), plan.Name.ValueString())) + plan.ThroughputCapacity = types.Int64Value(res["throughputCapacity"].(int64)) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} +func (d *GooglePubsubLiteReservationFWResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data GooglePubsubLiteReservationModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + // Use provider_meta to set User-Agent + userAgent := fwtransport.GenerateFrameworkUserAgentString(metaData, d.providerConfig.UserAgent) + + obj := make(map[string]interface{}) + + data.Project = fwresource.GetProjectFramework(data.Project, types.StringValue(d.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + data.Region = fwresource.GetRegionFramework(data.Region, types.StringValue(d.providerConfig.Region), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + billingProject := data.Project + + var schemaDefaultVals fwtransport.DefaultVars + schemaDefaultVals.Project = data.Project + schemaDefaultVals.Region = data.Region + + url := fwtransport.ReplaceVars(ctx, req, &resp.Diagnostics, schemaDefaultVals, d.providerConfig, "{{PubSubLiteBasePath}}projects/{{project}}/locations/{{region}}/instances/{{name}}") + + if resp.Diagnostics.HasError() { + return + } + tflog.Trace(ctx, fmt.Sprintf("[DEBUG] Deleting Reservation: %#v", obj)) + + headers := make(http.Header) + res := fwtransport.SendRequest(fwtransport.SendRequestOptions{ + Config: d.providerConfig, + Method: "DELETE", + Project: billingProject.ValueString(), + RawURL: url, + UserAgent: userAgent, + Body: obj, + Headers: headers, + }, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("[DEBUG] Deleted Reservation: %#v", res)) +} diff --git a/mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation_test.go b/mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation_test.go new file mode 100644 index 000000000000..e4507dfaec41 --- /dev/null +++ b/mmv1/third_party/terraform/services/pubsublite/fw_resource_pubsub_lite_reservation_test.go @@ -0,0 +1,56 @@ +package pubsublite_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccResourceFWPubsubLiteReservation_basic(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccResourceFWPubsubLiteReservation_basic(context), + }, + { + Config: testAccResourceFWPubsubLiteReservation_upgrade(context), + }, + }, + }) +} + +func testAccResourceFWPubsubLiteReservation_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_fwprovider_pubsub_lite_reservation" "basic" { + name = "tf-test-example-reservation%{random_suffix}" + region = "us-central1" + project = data.google_project.project.number + throughput_capacity = 2 +} + +data "google_project" "project" { +} +`, context) +} + +func testAccResourceFWPubsubLiteReservation_upgrade(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_fwprovider_pubsub_lite_reservation" "basic" { + name = "tf-test-example-reservation%{random_suffix}" + region = "us-central1" + project = data.google_project.project.number + throughput_capacity = 3 +} + +data "google_project" "project" { +} +`, context) +} From 7fc6cbef724d79cef7c697b5b281471a71f024fe Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 13 Mar 2025 11:11:57 -0500 Subject: [PATCH 4/8] Framework SQL User resource draft --- .../terraform/acctest/vcr_utils.go | 12 + mmv1/third_party/terraform/go.mod | 1 + mmv1/third_party/terraform/go.sum | 2 + .../services/sql/fw_resource_sql_user.go | 507 ++++++++++++++++++ .../services/sql/fw_resource_sql_user_test.go | 90 ++++ 5 files changed, 612 insertions(+) create mode 100644 mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go create mode 100644 mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go diff --git a/mmv1/third_party/terraform/acctest/vcr_utils.go b/mmv1/third_party/terraform/acctest/vcr_utils.go index 2107ca354320..c02ac955ac4c 100644 --- a/mmv1/third_party/terraform/acctest/vcr_utils.go +++ b/mmv1/third_party/terraform/acctest/vcr_utils.go @@ -25,6 +25,7 @@ import ( tpgprovider "github.com/hashicorp/terraform-provider-google/google/provider" "github.com/hashicorp/terraform-provider-google/google/services/compute" "github.com/hashicorp/terraform-provider-google/google/services/pubsublite" + "github.com/hashicorp/terraform-provider-google/google/services/sql" "github.com/hashicorp/terraform-provider-google/google/tpgresource" transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" @@ -32,6 +33,8 @@ import ( "github.com/dnaeon/go-vcr/recorder" "github.com/hashicorp/terraform-plugin-framework/datasource" + fwResource "github.com/hashicorp/terraform-plugin-framework/resource" + fwDiags "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/provider" fwResource "github.com/hashicorp/terraform-plugin-framework/resource" @@ -409,10 +412,19 @@ func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() dataso return ds } +<<<<<<< HEAD func (p *frameworkTestProvider) Resources(ctx context.Context) []func() fwResource.Resource { r := p.FrameworkProvider.Resources(ctx) r = append(r, pubsublite.NewGooglePubsubLiteReservationFWResource) // google_fwprovider_pubsub_lite_reservation return r +======= +// Resources overrides the provider's Resources function so that we can append test-specific resources +// Similar to the Datasources override +func (p *frameworkTestProvider) Resources(ctx context.Context) []func() fwResource.Resource { + rs := p.FrameworkProvider.Resources(ctx) + rs = append(rs, sql.NewSQLUserFWResource) // google_fw_sql_user + return rs +>>>>>>> 1d5c7ed64 (Framework SQL User resource draft) } // GetSDKProvider gets the SDK provider for use in acceptance tests diff --git a/mmv1/third_party/terraform/go.mod b/mmv1/third_party/terraform/go.mod index 165b15a4bf83..69f7b361fc01 100644 --- a/mmv1/third_party/terraform/go.mod +++ b/mmv1/third_party/terraform/go.mod @@ -79,6 +79,7 @@ require ( github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.22.0 // indirect + github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.4 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/mmv1/third_party/terraform/go.sum b/mmv1/third_party/terraform/go.sum index 387e9f17ac72..0b2e96edc871 100644 --- a/mmv1/third_party/terraform/go.sum +++ b/mmv1/third_party/terraform/go.sum @@ -177,6 +177,8 @@ github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4 github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 h1:I/N0g/eLZ1ZkLZXUQ0oRSXa8YG/EF0CEuQP1wXdrzKw= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0/go.mod h1:t339KhmxnaF4SzdpxmqW8HnQBHVGYazwtfxU0qCs4eE= 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.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= diff --git a/mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go new file mode 100644 index 000000000000..5e536900be96 --- /dev/null +++ b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user.go @@ -0,0 +1,507 @@ +package sql + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwresource" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + "github.com/hashicorp/terraform-provider-google/google/transport" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + sqladmin "google.golang.org/api/sqladmin/v1beta4" +) + +var ( + _ resource.Resource = &SQLUserFWResource{} + _ resource.ResourceWithConfigure = &SQLUserFWResource{} +) + +func NewSQLUserFWResource() resource.Resource { + return &SQLUserFWResource{} +} + +type SQLUserFWResource struct { + client *sqladmin.Service + providerConfig *transport_tpg.Config +} + +type SQLUserModel struct { + Id types.String `tfsdk:"id"` + Project types.String `tfsdk:"project"` + Name types.String `tfsdk:"name"` + Host types.String `tfsdk:"host"` + Instance types.String `tfsdk:"instance"` + Password types.String `tfsdk:"password"` + // PasswordWO types.String `tfsdk:"password_wo"` + // PasswordWOVersion types.String `tfsdk:"password_wo_version"` + Type types.String `tfsdk:"type"` + // SqlServerUserDetails types.List `tfsdk:"sql_server_user_details"` + // PasswordPolicy types.List `tfsdk:"password_policy"` + // DeletionPolicy types.String `tfsdk:"deletion_policy"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// Metadata returns the resource type name. +func (d *SQLUserFWResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_fw_sql_user" +} + +func (r *SQLUserFWResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + p, ok := req.ProviderData.(*transport_tpg.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *transport_tpg.Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = p.NewSqlAdminClient(p.UserAgent) + if resp.Diagnostics.HasError() { + return + } + r.providerConfig = p +} + +func (d *SQLUserFWResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A resource to represent a SQL User object.", + + Attributes: map[string]schema.Attribute{ + "project": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "host": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "instance": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: `The name of the user. Changing this forces a new resource to be created.`, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + SQLUserNameIAMPlanModifier(), + }, + }, + "password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + }, + "type": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + // TODO DiffSuppressFunc: tpgresource.EmptyOrDefaultStringSuppress("BUILT_IN"), + }, + }, + // This is included for backwards compatibility with the original, SDK-implemented resource. + "id": schema.StringAttribute{ + Description: "Project identifier", + MarkdownDescription: "Project identifier", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + }), + }, + } +} + +func (r *SQLUserFWResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data SQLUserModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + project := fwresource.GetProjectFramework(data.Project, types.StringValue(r.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + nameData, diags := data.Name.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + instanceData, diags := data.Instance.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + hostData, diags := data.Host.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + typeData, diags := data.Type.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + passwordData, diags := data.Password.ToStringValue(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + createTimeout, diags := data.Timeouts.Create(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user := &sqladmin.User{ + Name: nameData.ValueString(), + Instance: instanceData.ValueString(), + Password: passwordData.ValueString(), + Host: hostData.ValueString(), + Type: typeData.ValueString(), + } + + transport_tpg.MutexStore.Lock(instanceMutexKey(project.ValueString(), instanceData.ValueString())) + defer transport_tpg.MutexStore.Unlock(instanceMutexKey(project.ValueString(), instanceData.ValueString())) + + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + // TODO host check logic + + var op *sqladmin.Operation + var err error + insertFunc := func() error { + op, err = r.client.Users.Insert(project.ValueString(), instanceData.ValueString(), + user).Do() + return err + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: insertFunc, + Timeout: createTimeout, + }) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error, failed to insert "+ + "user %s into instance %s", nameData.ValueString(), instanceData.ValueString()), err.Error()) + return + } + + err = SqlAdminOperationWaitTime(r.providerConfig, op, project.ValueString(), "Insert User", r.client.UserAgent, createTimeout) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error, failure waiting to insert "+ + "user %s into instance %s", nameData.ValueString(), instanceData.ValueString()), err.Error()) + return + } + + tflog.Trace(ctx, "created sql user resource") + + // This will include a double-slash (//) for postgres instances, + // for which user.Host is an empty string. That's okay. + data.Id = types.StringValue(fmt.Sprintf("%s/%s/%s", user.Name, user.Host, user.Instance)) + data.Project = project + + // read back sql user + r.SQLUserRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SQLUserFWResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data SQLUserModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + tflog.Trace(ctx, "read sql user resource") + + // read back sql user + r.SQLUserRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SQLUserFWResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var old, new SQLUserModel + var metaData *fwmodels.ProviderMetaModel + + resp.Diagnostics.Append(req.State.Get(ctx, &old)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.Plan.Get(ctx, &new)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + if !old.Password.Equal(new.Password) { + project := new.Project.ValueString() + instance := new.Instance.ValueString() + name := new.Name.ValueString() + host := new.Host.ValueString() + password := new.Password.ValueString() + + updateTimeout, diags := new.Timeouts.Update(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user := &sqladmin.User{ + Name: name, + Instance: instance, + Password: password, + } + transport_tpg.MutexStore.Lock(instanceMutexKey(project, instance)) + defer transport_tpg.MutexStore.Unlock(instanceMutexKey(project, instance)) + var op *sqladmin.Operation + var err error + updateFunc := func() error { + op, err = r.client.Users.Update(project, instance, user).Host(host).Name(name).Do() + return err + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: updateFunc, + Timeout: updateTimeout, + }) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failed to update"+ + "user %s in instance %s", name, instance), err.Error()) + return + } + + err = SqlAdminOperationWaitTime(r.providerConfig, op, project, "Update User", r.client.UserAgent, updateTimeout) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failure waiting for update"+ + "user %s in instance %s", name, instance), err.Error()) + return + } + + // read back sql user + r.SQLUserRefresh(ctx, &new, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &new)...) +} + +func (r *SQLUserFWResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data SQLUserModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + project := data.Project.ValueString() + instance := data.Instance.ValueString() + name := data.Name.ValueString() + host := data.Host.ValueString() + + deleteTimeout, diags := data.Timeouts.Delete(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + transport_tpg.MutexStore.Lock(instanceMutexKey(project, instance)) + defer transport_tpg.MutexStore.Unlock(instanceMutexKey(project, instance)) + var op *sqladmin.Operation + var err error + deleteFunc := func() error { + op, err = r.client.Users.Delete(project, instance).Host(host).Name(name).Do() + return err + } + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: deleteFunc, + Timeout: deleteTimeout, + }) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("failed to delete"+ + "user %s in instance %s", name, instance), err.Error()) + return + } + + err = SqlAdminOperationWaitTime(r.providerConfig, op, project, "Delete User", r.client.UserAgent, deleteTimeout) + + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error, failure waiting to delete "+ + "user %s", name), err.Error()) + return + } +} + +func (r *SQLUserFWResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + + // TODO recreate all import cases + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: project/instance/host/name. Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("host"), idParts[2])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[3])...) +} + +func (r *SQLUserFWResource) SQLUserRefresh(ctx context.Context, data *SQLUserModel, state *tfsdk.State, diag *diag.Diagnostics) { + userReadResp, err := r.client.Users.Get(data.Project.ValueString(), data.Instance.ValueString(), data.Name.ValueString()).Host(data.Host.ValueString()).Do() + if err != nil { + // Treat HTTP 404 Not Found status as a signal to recreate resource + // and return early + if userReadResp != nil && transport.IsGoogleApiErrorWithCode(err, userReadResp.HTTPStatusCode) { + tflog.Trace(ctx, "sql user resource not found, removing from state") + state.RemoveResource(ctx) + return + } + diag.AddError(fmt.Sprintf("Error, failure waiting to read "+ + "user %s", data.Name.ValueString()), err.Error()) + return + } + + id := fmt.Sprintf("projects/%s/global/networks/%s", userReadResp.Project, userReadResp.Name) + data.Id = types.StringValue(id) + data.Project = types.StringValue(userReadResp.Project) + data.Instance = types.StringValue(userReadResp.Instance) + if userReadResp.Host != "" { + data.Host = types.StringValue(userReadResp.Host) + } + if userReadResp.Type != "" { + data.Type = types.StringValue(userReadResp.Type) + } +} + +// Plan Modifiers +func SQLUserNameIAMPlanModifier() planmodifier.String { + return &sqlUserNameIAMPlanModifier{} +} + +type sqlUserNameIAMPlanModifier struct { +} + +func (d *sqlUserNameIAMPlanModifier) Description(ctx context.Context) string { + return "Suppresses name diffs for IAM user types." +} +func (d *sqlUserNameIAMPlanModifier) MarkdownDescription(ctx context.Context) string { + return d.Description(ctx) +} + +// Plan modifier to emulate the SDK diffSuppressIamUserName +func (d *sqlUserNameIAMPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // Retrieve relevant fields + var oldName types.String + diags := req.State.GetAttribute(ctx, path.Root("name"), &oldName) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var newName types.String + diags = req.Plan.GetAttribute(ctx, path.Root("name"), &newName) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var userType types.String + diags = req.Plan.GetAttribute(ctx, path.Root("type"), &userType) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Old diff suppress logic + strippedNewName := strings.Split(newName.ValueString(), "@")[0] + + if oldName.ValueString() == strippedNewName && strings.Contains(userType.ValueString(), "IAM") { + // Suppress the diff by setting the planned value to the old value + resp.PlanValue = oldName + } +} diff --git a/mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go new file mode 100644 index 000000000000..80e78c4a0316 --- /dev/null +++ b/mmv1/third_party/terraform/services/sql/fw_resource_sql_user_test.go @@ -0,0 +1,90 @@ +package sql_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" +) + +func TestAccSqlUserFW_mysql(t *testing.T) { + // Multiple fine-grained resources + acctest.SkipIfVcr(t) + t.Parallel() + + instance := fmt.Sprintf("tf-test-%d", acctest.RandInt(t)) + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlUserDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlUserFW_mysql(instance, "password"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user1"), + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user2"), + ), + }, + { + // Update password + Config: testGoogleSqlUserFW_mysql(instance, "new_password"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user1"), + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user2"), + testAccCheckGoogleSqlUserExists(t, "google_fw_sql_user.user3"), + ), + }, + { + ResourceName: "google_fw_sql_user.user2", + ImportStateId: fmt.Sprintf("%s/%s/gmail.com/admin", envvar.GetTestProjectFromEnv(), instance), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + { + ResourceName: "google_fw_sql_user.user3", + ImportStateId: fmt.Sprintf("%s/%s/10.0.0.0/24/admin", envvar.GetTestProjectFromEnv(), instance), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, + }, + }) +} + +func testGoogleSqlUserFW_mysql(instance, password string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + settings { + tier = "db-f1-micro" + } +} + +resource "google_fw_sql_user" "user1" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "google.com" + password = "%s" +} + +resource "google_fw_sql_user" "user2" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "gmail.com" + password = "hunter2" +} + +resource "google_fw_sql_user" "user3" { + name = "admin" + instance = google_sql_database_instance.instance.name + host = "10.0.0.0/24" + password = "hunter3" +} +`, instance, password) +} From 4c78e3e9c887b0b2028bbb01dc4abf1d44510036 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 27 Mar 2025 15:55:47 -0500 Subject: [PATCH 5/8] initial template + CRUD --- mmv1/api/resource.go | 3 + mmv1/api/type.go | 48 +++ mmv1/products/pubsublite/Reservation.yaml | 1 + mmv1/provider/template_data.go | 9 + mmv1/provider/terraform.go | 9 +- mmv1/templates/terraform/resource_fw.go.tmpl | 398 ++++++++++++++++++ .../terraform/schema_property_fw.go.tmpl | 52 +++ 7 files changed, 518 insertions(+), 2 deletions(-) create mode 100644 mmv1/templates/terraform/resource_fw.go.tmpl create mode 100644 mmv1/templates/terraform/schema_property_fw.go.tmpl diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index 8fd13845051b..f6fa8abb7e20 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -304,6 +304,9 @@ type Resource struct { // control if a resource is continuously generated from public OpenAPI docs AutogenStatus string `yaml:"autogen_status"` + // If true, this resource generates with the new plugin framework resource template + FrameworkResource bool `yaml:"plugin_framework,omitempty"` + // The three groups of []*Type fields are expected to be strictly ordered within a yaml file // in the sequence of Virtual Fields -> Parameters -> Properties diff --git a/mmv1/api/type.go b/mmv1/api/type.go index 573730146017..1d4831cbdbe2 100644 --- a/mmv1/api/type.go +++ b/mmv1/api/type.go @@ -515,6 +515,15 @@ func (t Type) ResourceType() string { return path[len(path)-1] } +func (t Type) FWResourceType() string { + r := t.ResourceRef() + if r == nil { + return "" + } + path := strings.Split(r.BaseUrl, "/") + return path[len(path)-1] +} + // TODO rewrite: validation // func (t *Type) check_default_value_property() { // return if @default_value.nil? @@ -785,6 +794,45 @@ func (t Type) TFType(s string) string { return "schema.TypeString" } +func (t Type) GetFWType() string { + switch t.Type { + case "Boolean": + return "Bool" + case "Double": + return "Float64" + case "Integer": + return "Int64" + case "String": + return "String" + case "Time": + return "String" + case "Enum": + return "String" + case "ResourceRef": + return "String" + case "NestedObject": + return "Nested" + case "Array": + return "List" + case "KeyValuePairs": + return "Map" + case "KeyValueLabels": + return "Map" + case "KeyValueTerraformLabels": + return "Map" + case "KeyValueEffectiveLabels": + return "Map" + case "KeyValueAnnotations": + return "Map" + case "Map": + return "Map" + case "Fingerprint": + return "String" + } + + return "String" +} + // TODO rewrite: validation // // Represents an enum, and store is valid values // class Enum < Primitive diff --git a/mmv1/products/pubsublite/Reservation.yaml b/mmv1/products/pubsublite/Reservation.yaml index eed9f6e4eedb..fac813c0a5ee 100644 --- a/mmv1/products/pubsublite/Reservation.yaml +++ b/mmv1/products/pubsublite/Reservation.yaml @@ -15,6 +15,7 @@ name: 'Reservation' description: | A named resource representing a shared pool of capacity. +plugin_framework: true references: guides: 'Managing Reservations': 'https://cloud.google.com/pubsub/lite/docs/reservations' diff --git a/mmv1/provider/template_data.go b/mmv1/provider/template_data.go index 5db15f6e51fc..172bd3f34ee7 100644 --- a/mmv1/provider/template_data.go +++ b/mmv1/provider/template_data.go @@ -86,6 +86,15 @@ func (td *TemplateData) GenerateResourceFile(filePath string, resource api.Resou td.GenerateFile(filePath, templatePath, resource, true, templates...) } +func (td *TemplateData) GenerateFWResourceFile(filePath string, resource api.Resource) { + templatePath := "templates/terraform/resource_fw.go.tmpl" + templates := []string{ + templatePath, + "templates/terraform/schema_property_fw.go.tmpl", + } + td.GenerateFile(filePath, templatePath, resource, true, templates...) +} + func (td *TemplateData) GenerateMetadataFile(filePath string, resource api.Resource) { templatePath := "templates/terraform/metadata.yaml.tmpl" templates := []string{ diff --git a/mmv1/provider/terraform.go b/mmv1/provider/terraform.go index 75a077105bc4..37ccc795ad29 100644 --- a/mmv1/provider/terraform.go +++ b/mmv1/provider/terraform.go @@ -125,8 +125,13 @@ func (t *Terraform) GenerateResource(object api.Resource, templateData TemplateD if err := os.MkdirAll(targetFolder, os.ModePerm); err != nil { log.Println(fmt.Errorf("error creating parent directory %v: %v", targetFolder, err)) } - targetFilePath := path.Join(targetFolder, fmt.Sprintf("resource_%s.go", t.ResourceGoFilename(object))) - templateData.GenerateResourceFile(targetFilePath, object) + if object.FrameworkResource { + targetFilePath := path.Join(targetFolder, fmt.Sprintf("resource_fw_%s.go", t.ResourceGoFilename(object))) + templateData.GenerateFWResourceFile(targetFilePath, object) + } else { + targetFilePath := path.Join(targetFolder, fmt.Sprintf("resource_%s.go", t.ResourceGoFilename(object))) + templateData.GenerateResourceFile(targetFilePath, object) + } } if generateDocs { diff --git a/mmv1/templates/terraform/resource_fw.go.tmpl b/mmv1/templates/terraform/resource_fw.go.tmpl new file mode 100644 index 000000000000..d0348f94ac5b --- /dev/null +++ b/mmv1/templates/terraform/resource_fw.go.tmpl @@ -0,0 +1,398 @@ +{{/* The license inside this block applies to this file + Copyright 2025 Google LLC. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ -}} +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +{{/*{{$.CodeHeader TemplatePath}}*/}} + +package {{ lower $.ProductMetadata.Name }} + +import ( + + "fmt" + "log" + "net/http" + "reflect" +{{- if $.SupportsIndirectUserProjectOverride }} + "regexp" +{{- end }} +{{- if or (and (not $.Immutable) ($.UpdateMask)) $.LegacyLongFormProject }} + "strings" +{{- end }} + "time" + +{{/* # We list all the v2 imports here, because we run 'goimports' to guess the correct */}} +{{/* # set of imports, which will never guess the major version correctly. */}} +{{/* + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" + */}} + "github.com/hashicorp/go-cty/cty" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-provider-google/google/fwmodels" + "github.com/hashicorp/terraform-provider-google/google/fwresource" + "github.com/hashicorp/terraform-provider-google/google/fwtransport" + + "{{ $.ImportPath }}/tpgresource" + transport_tpg "{{ $.ImportPath }}/transport" + "{{ $.ImportPath }}/verify" + +{{ if $.FlattenedProperties }} + "google.golang.org/api/googleapi" +{{- end}} +) + +{{if $.CustomCode.Constants -}} + {{- $.CustomTemplate $.CustomCode.Constants true -}} +{{- end}} + +var ( + _ resource.Resource = &{{$.ResourceName}}FWResource{} + _ resource.ResourceWithConfigure = &{{$.ResourceName}}FWResource{} +) + +func New{{$.ResourceName}}FWResource() resource.Resource { + return &{{$.ResourceName}}FWResource{} +} + +type {{$.ResourceName}}FWResource struct { + {{/*client *sqladmin.Service*/}} + providerConfig *transport_tpg.Config +} + +type {{$.ResourceName}}FWModel struct { + {{- range $prop := $.OrderProperties $.AllUserProperties }} + {{camelize $prop.Name "upper"}} types.{{$prop.GetFWType}} `tfsdk:"{{underscore $prop.Name}}"` + {{- end }} +} + +// Metadata returns the resource type name. +func (d *{{$.ResourceName}}FWResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_fw_{{ underscore $.ResourceName}}" +} + +func (r *{{$.ResourceName}}FWResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + p, ok := req.ProviderData.(*transport_tpg.Config) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *transport_tpg.Config, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + {{/* TODO non-client equivalent? */}} + {{/* + r.client = p.NewSqlAdminClient(p.UserAgent) + if resp.Diagnostics.HasError() { + return + }*/}} + r.providerConfig = p +} + +func (d *{{$.ResourceName}}FWResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "A resource to represent a SQL User object.", + + Attributes: map[string]schema.Attribute{ +{{- range $prop := $.OrderProperties $.AllUserProperties }} + {{template "SchemaFieldsFW" $prop -}} +{{- end }} +{{- range $prop := $.VirtualFields }} + {{template "SchemaFieldsFW" $prop -}} +{{- end }} +{{- if $.CustomCode.ExtraSchemaEntry }} + {{ $.CustomTemplate $.CustomCode.ExtraSchemaEntry false -}} +{{- end}} +{{ if $.HasProject -}} + "project": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, +{{- end}} +{{- if $.HasSelfLink }} + "self_link": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, +{{- end}} + // This is included for backwards compatibility with the original, SDK-implemented resource. + "id": schema.StringAttribute{ + Description: "Project identifier", + MarkdownDescription: "Project identifier", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *{{$.ResourceName}}FWResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data {{$.ResourceName}}FWModel + var metaData *fwmodels.ProviderMetaModel +{{ if $.CustomCode.CustomCreate -}} + {{ $.CustomTemplate $.CustomCode.CustomCreate false -}} +{{ else -}} + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + project := fwresource.GetProjectFramework(data.Project, types.StringValue(r.providerConfig.Project), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + obj := make(map[string]interface{}) + +{{- range $prop := $.OrderProperties $.AllUserProperties }} + {{$prop.ApiName}}Prop, diags := data.{{camelize $prop.Name "upper"}}.To{{$prop.GetFWType}}Value(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + obj["{{ $prop.ApiName -}}"] = {{ $prop.ApiName -}}Prop +{{- end }} + + + createTimeout, diags := data.Timeouts.Create(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + url, err := tpgresource.ReplaceVars{{if $.LegacyLongFormProject -}}ForId{{ end -}}(d, config, "{{"{{"}}{{$.ProductMetadata.Name}}BasePath{{"}}"}}{{$.CreateUri}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new {{ $.Name -}}: %#v", obj) + + headers := make(http.Header) +{{- if $.CustomCode.PreCreate }} + {{ $.CustomTemplate $.CustomCode.PreCreate false -}} +{{- end}} + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "{{ upper $.CreateVerb -}}", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: createTimeout, + Headers: headers, +{{- if $.ErrorRetryPredicates }} + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{{"{"}}{{ join $.ErrorRetryPredicates "," -}}{{"}"}}, +{{- end}} +{{- if $.ErrorAbortPredicates }} + ErrorAbortPredicates: []transport_tpg.RetryErrorPredicateFunc{{"{"}}{{ join $.ErrorAbortPredicates "," -}}{{"}"}}, +{{- end}} + }) + if err != nil { +{{- if and ($.CustomCode.PostCreateFailure) (not $.GetAsync) -}} + resource{{ $.ResourceName -}}PostCreateFailure(d, meta) +{{- end}} + resp.Diagnostics.AppendError(fmt.Sprintf("Error creating {{ $.Name -}}: %s", err)) + return + } + + tflog.Trace(ctx, "created {{$.Name}} resource") + + data.Id = types.StringValue("{{ $.IdFormat -}}") + data.Project = project + + + // read back {{$.Name}} + r.{{$.ResourceName}}FWRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + +{{ end }} {{/* if CustomCreate */}} +} + + +func (r *{{$.ResourceName}}FWResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data {{$.ResourceName}}FWModel + var metaData *fwmodels.ProviderMetaModel + + // Read Provider meta into the meta model + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &metaData)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Use provider_meta to set User-Agent + r.client.UserAgent = fwtransport.GenerateFrameworkUserAgentString(metaData, r.client.UserAgent) + + tflog.Trace(ctx, "read {{$.Name}} resource") + + // read back {{$.Name}} + r.{{$.ResourceName}}FWRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + + +func (r *{{$.ResourceName}}FWResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var old, new {{$.ResourceName}}FWModel + var metaData *fwmodels.ProviderMetaModel + + resp.Diagnostics.Append(req.State.Get(ctx, &old)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.Plan.Get(ctx, &new)...) + if resp.Diagnostics.HasError() { + return + } + + {{/* Update old/new checks */}} + + // read back {{$.Name}} + r.{{$.ResourceName}}FWRefresh(ctx, &data, &resp.State, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &new)...) +} + + +func (r *{{$.ResourceName}}FWResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data {{$.ResourceName}}FWModel + +{{- if $.ExcludeDelete }} + log.Printf("[WARNING] {{ $.ProductMetadata.Name }}{{" "}}{{ $.Name }} resources" + + " cannot be deleted from Google Cloud. The resource %s will be removed from Terraform" + + " state, but will still be present on Google Cloud.", d.Id()) + d.SetId("") + + return nil +{{- else }} + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + deleteTimeout, diags := data.Timeouts.Delete(ctx, 20*time.Minute) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + url, err := tpgresource.ReplaceVars{{if $.LegacyLongFormProject -}}ForId{{ end -}}(d, config, "{{"{{"}}{{$.ProductMetadata.Name}}BasePath{{"}}"}}{{$.DeleteUri}}") + if err != nil { + return err + } +{{ if $.CustomCode.CustomDelete }} +{{ $.CustomTemplate $.CustomCode.CustomDelete false -}} +{{- else }} + headers := make(http.Header) + {{- if $.CustomCode.PreDelete }} + {{ $.CustomTemplate $.CustomCode.PreDelete false -}} + {{- end }} + + log.Printf("[DEBUG] Deleting {{ $.Name }} %q", d.Id()) + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "{{ camelize $.DeleteVerb "upper" -}}", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: deleteTimeout, + Headers: headers, + {{- if $.ErrorRetryPredicates }} + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{{"{"}}{{- join $.ErrorRetryPredicates "," -}}{{"}"}}, + {{- end }} + {{- if $.ErrorAbortPredicates }} + ErrorAbortPredicates: []transport_tpg.RetryErrorPredicateFunc{{"{"}}{{- join $.ErrorAbortPredicates "," -}}{{"}"}}, + {{- end }} + }) + if err != nil { + resp.Diagnostics.AppendError(fmt.Sprintf("Error deleting {{ $.Name -}}: %s", err)) + return + } +{{- if $.CustomCode.PostDelete }} + {{ $.CustomTemplate $.CustomCode.PostDelete false -}} +{{- end }} + + log.Printf("[DEBUG] Finished deleting {{ $.Name }} %q: %#v", data.Id, res) + + +{{- end }}{{/* if CustomCode.CustomDelete */}} +{{- end }}{{/* if ExcludeDelete */}} +} + +func (r *{{$.ResourceName}}FWResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *{{$.ResourceName}}FWResource) {{$.ResourceName}}FWRefresh(ctx context.Context, data *{{$.ResourceName}}FWModel, state *tfsdk.State, diag *diag.Diagnostics) { + // TODO refresh +} \ No newline at end of file diff --git a/mmv1/templates/terraform/schema_property_fw.go.tmpl b/mmv1/templates/terraform/schema_property_fw.go.tmpl new file mode 100644 index 000000000000..03bcbb82e896 --- /dev/null +++ b/mmv1/templates/terraform/schema_property_fw.go.tmpl @@ -0,0 +1,52 @@ +{{/*# The license inside this block applies to this file. + # Copyright 2024 Google Inc. + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +*/}} +{{- define "SchemaFieldsFW"}} +{{- if .FlattenObject -}} + {{- range $prop := .ResourceMetadata.OrderProperties .UserProperties -}} + {{ template "SchemaFieldsFW" $prop }} + {{ end -}} +{{- else -}} +"{{underscore .Name -}}": schema.{{.GetFWType}}Attribute{ +{{ if .DefaultFromApi -}} + Optional: true, + Computed: true, +{{ else if .Required -}} + Required: true, +{{ else if .Output -}} + Computed: true, +{{ else -}} + Optional: true, +{{ end -}} +{{ if .DeprecationMessage -}} + DeprecationMessage: "{{ .DeprecationMessage }}", +{{ end -}} +{{ if .Sensitive -}} + Sensitive: true, +{{ end -}} +{{ if or .IsForceNew .DefaultFromApi -}} + PlanModifiers: []planmodifier.{{.GetFWType}}{ + + {{ if .IsForceNew -}} + {{lower .GetFWType}}planmodifier.RequiresReplace(), + {{ end -}} + + {{ if .DefaultFromApi -}} + {{lower .GetFWType}}planmodifier.UseStateForUnknown(), + {{ end -}} + }, +{{ end -}} +}, +{{- end -}} +{{- end -}} \ No newline at end of file From 41ccbf4c103a0533ea3574ee612f635282cacbd8 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 8 May 2025 16:38:51 -0500 Subject: [PATCH 6/8] fix git conflict --- mmv1/third_party/terraform/acctest/vcr_utils.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/mmv1/third_party/terraform/acctest/vcr_utils.go b/mmv1/third_party/terraform/acctest/vcr_utils.go index c02ac955ac4c..009c4ec43435 100644 --- a/mmv1/third_party/terraform/acctest/vcr_utils.go +++ b/mmv1/third_party/terraform/acctest/vcr_utils.go @@ -412,19 +412,10 @@ func (p *frameworkTestProvider) DataSources(ctx context.Context) []func() dataso return ds } -<<<<<<< HEAD func (p *frameworkTestProvider) Resources(ctx context.Context) []func() fwResource.Resource { r := p.FrameworkProvider.Resources(ctx) - r = append(r, pubsublite.NewGooglePubsubLiteReservationFWResource) // google_fwprovider_pubsub_lite_reservation + r = append(r, pubsublite.NewGooglePubsubLiteReservationFWResource, sql.NewSQLUserFWResource) // google_fwprovider_pubsub_lite_reservation return r -======= -// Resources overrides the provider's Resources function so that we can append test-specific resources -// Similar to the Datasources override -func (p *frameworkTestProvider) Resources(ctx context.Context) []func() fwResource.Resource { - rs := p.FrameworkProvider.Resources(ctx) - rs = append(rs, sql.NewSQLUserFWResource) // google_fw_sql_user - return rs ->>>>>>> 1d5c7ed64 (Framework SQL User resource draft) } // GetSDKProvider gets the SDK provider for use in acceptance tests From 80fa0657afaf81fbe2d1352812dbb91920f5844b Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 8 May 2025 16:55:00 -0500 Subject: [PATCH 7/8] stop pubsublite framework generation as template is WIP --- mmv1/products/pubsublite/Reservation.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/mmv1/products/pubsublite/Reservation.yaml b/mmv1/products/pubsublite/Reservation.yaml index fac813c0a5ee..eed9f6e4eedb 100644 --- a/mmv1/products/pubsublite/Reservation.yaml +++ b/mmv1/products/pubsublite/Reservation.yaml @@ -15,7 +15,6 @@ name: 'Reservation' description: | A named resource representing a shared pool of capacity. -plugin_framework: true references: guides: 'Managing Reservations': 'https://cloud.google.com/pubsub/lite/docs/reservations' From 9e283700d5764bce69dcc377651912f61d0a8883 Mon Sep 17 00:00:00 2001 From: Cameron Thornton Date: Thu, 8 May 2025 17:44:21 -0500 Subject: [PATCH 8/8] extra import --- mmv1/third_party/terraform/acctest/vcr_utils.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mmv1/third_party/terraform/acctest/vcr_utils.go b/mmv1/third_party/terraform/acctest/vcr_utils.go index 009c4ec43435..4096057ffd7b 100644 --- a/mmv1/third_party/terraform/acctest/vcr_utils.go +++ b/mmv1/third_party/terraform/acctest/vcr_utils.go @@ -37,7 +37,6 @@ import ( fwDiags "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/provider" - fwResource "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag"