From 2ac26bdf22a557603a15b532770ff9f8f481f76e Mon Sep 17 00:00:00 2001 From: Teemu Matilainen Date: Sat, 2 Dec 2023 18:04:23 +0200 Subject: [PATCH 1/8] Add `tfe_registry_gpg_key` resource Manages a public key of the GPG key pair used to sign releases of private providers in the private registry. --- CHANGELOG.md | 1 + internal/provider/attribute_helpers.go | 41 +++ internal/provider/gpg_key.go | 33 +++ internal/provider/provider_next.go | 1 + .../provider/resource_tfe_registry_gpg_key.go | 249 ++++++++++++++++++ .../resource_tfe_registry_gpg_key_test.go | 108 ++++++++ internal/provider/resource_tfe_variable.go | 7 - website/docs/r/registry_gpg_key.html.markdown | 44 ++++ 8 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 internal/provider/attribute_helpers.go create mode 100644 internal/provider/gpg_key.go create mode 100644 internal/provider/resource_tfe_registry_gpg_key.go create mode 100644 internal/provider/resource_tfe_registry_gpg_key_test.go create mode 100644 website/docs/r/registry_gpg_key.html.markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f9b4f13..986fba354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ FEATURES: * `d/tfe_registry_module`: Add `vcs_repo.tags` and `vcs_repo.branch` attributes to allow configuration of `publishing_mechanism`. Add `test_config` to support running tests on `branch`-based registry modules, by @hashimoon [1096](https://github.com/hashicorp/terraform-provider-tfe/pull/1096) * **New Resource**: `r/tfe_organization_default_execution_mode` is a new resource to set the `default_execution_mode` and `default_agent_pool_id` for an organization, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' * `r/tfe_workspace`: Now uses the organization's `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode`, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' +* **New Resource**: `r/tfe_registry_gpg_key` is a new resource for managing private registry GPG keys, by @tmatilai ENHANCEMENTS: * `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133) diff --git a/internal/provider/attribute_helpers.go b/internal/provider/attribute_helpers.go new file mode 100644 index 000000000..0e424e68c --- /dev/null +++ b/internal/provider/attribute_helpers.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// AttrGettable is a small enabler for helper functions that need to read one +// attribute of a Configuration, Plan, or State. +type AttrGettable interface { + GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics +} + +// dataOrDefaultOrganization returns the value of the "organization" attribute +// from the Config/Plan/State data, defaulting to the provier configuration. +// If neither is set, an error is returned. +func (c *ConfiguredClient) dataOrDefaultOrganization(ctx context.Context, data AttrGettable, target *string) diag.Diagnostics { + schemaPath := path.Root("organization") + + var organization types.String + diags := data.GetAttribute(ctx, schemaPath, &organization) + if diags.HasError() { + return diags + } + + if !organization.IsNull() && !organization.IsUnknown() { + *target = organization.ValueString() + } else if c.Organization == "" { + diags.AddAttributeError(schemaPath, "No organization was specified on the resource or provider", "") + } else { + *target = c.Organization + } + + return diags +} diff --git a/internal/provider/gpg_key.go b/internal/provider/gpg_key.go new file mode 100644 index 000000000..ee25671e5 --- /dev/null +++ b/internal/provider/gpg_key.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "time" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// modelTFERegistryGPGKey maps the resource or data source schema data to a +// struct. +type modelTFERegistryGPGKey struct { + ID types.String `tfsdk:"id"` + Organization types.String `tfsdk:"organization"` + AsciiArmor types.String `tfsdk:"ascii_armor"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +// modelFromTFEVGPGKey builds a modelTFERegistryGPGKey struct from a +// tfe.GPGKey value. +func modelFromTFEVGPGKey(v *tfe.GPGKey) modelTFERegistryGPGKey { + return modelTFERegistryGPGKey{ + ID: types.StringValue(v.KeyID), + Organization: types.StringValue(v.Namespace), + AsciiArmor: types.StringValue(v.AsciiArmor), + CreatedAt: types.StringValue(v.CreatedAt.Format(time.RFC3339)), + UpdatedAt: types.StringValue(v.UpdatedAt.Format(time.RFC3339)), + } +} diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index a8b964b4d..d79c6993b 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -119,6 +119,7 @@ func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + NewRegistryGPGKeyResource, NewResourceVariable, NewSAMLSettingsResource, } diff --git a/internal/provider/resource_tfe_registry_gpg_key.go b/internal/provider/resource_tfe_registry_gpg_key.go new file mode 100644 index 000000000..8d1b49e0b --- /dev/null +++ b/internal/provider/resource_tfe_registry_gpg_key.go @@ -0,0 +1,249 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &resourceTFERegistryGPGKey{} +var _ resource.ResourceWithConfigure = &resourceTFERegistryGPGKey{} +var _ resource.ResourceWithImportState = &resourceTFERegistryGPGKey{} + +func NewRegistryGPGKeyResource() resource.Resource { + return &resourceTFERegistryGPGKey{} +} + +// resourceTFERegistryGPGKey implements the tfe_registry_gpg_key resource type +type resourceTFERegistryGPGKey struct { + config ConfiguredClient +} + +func (r *resourceTFERegistryGPGKey) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_registry_gpg_key" +} + +func (r *resourceTFERegistryGPGKey) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages a public key of the GPG key pair used to sign releases of private providers in the private registry.", + Version: 1, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the GPG key.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization. If omitted, organization must be defined in the provider config.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "ascii_armor": schema.StringAttribute{ + Description: "ASCII-armored representation of the GPG key.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "created_at": schema.StringAttribute{ + Description: "The time when the GPG key was created.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "updated_at": schema.StringAttribute{ + Description: "The time when the GPG key was last updated.", + Computed: true, + }, + }, + } +} + +// Configure implements resource.ResourceWithConfigure +func (r *resourceTFERegistryGPGKey) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected resource Configure type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + } + r.config = client +} + +func (r *resourceTFERegistryGPGKey) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan modelTFERegistryGPGKey + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + var organization string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Plan, &organization)...) + + if resp.Diagnostics.HasError() { + return + } + + options := tfe.GPGKeyCreateOptions{ + Type: "gpg-keys", + Namespace: organization, + AsciiArmor: plan.AsciiArmor.ValueString(), + } + + tflog.Debug(ctx, "Creating private registry GPG key") + key, err := r.config.Client.GPGKeys.Create(ctx, "private", options) + if err != nil { + resp.Diagnostics.AddError("Unable to create private registry GPG key", err.Error()) + return + } + + result := modelFromTFEVGPGKey(key) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceTFERegistryGPGKey) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state modelTFERegistryGPGKey + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + var organization string + resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.State, &organization)...) + + if resp.Diagnostics.HasError() { + return + } + + keyID := tfe.GPGKeyID{ + RegistryName: "private", + Namespace: organization, + KeyID: state.ID.ValueString(), + } + + tflog.Debug(ctx, "Reading private registry GPG key") + key, err := r.config.Client.GPGKeys.Read(ctx, keyID) + if err != nil { + resp.Diagnostics.AddError("Unable to read private registry GPG key", err.Error()) + return + } + + result := modelFromTFEVGPGKey(key) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceTFERegistryGPGKey) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan modelTFERegistryGPGKey + var state modelTFERegistryGPGKey + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + keyID := tfe.GPGKeyID{ + RegistryName: "private", + Namespace: state.Organization.ValueString(), // The old namespace + KeyID: plan.ID.ValueString(), + } + options := tfe.GPGKeyUpdateOptions{ + Type: "gpg-keys", + Namespace: plan.Organization.ValueString(), // The new namespace + } + + tflog.Debug(ctx, "Updating private registry GPG key") + key, err := r.config.Client.GPGKeys.Update(ctx, keyID, options) + if err != nil { + resp.Diagnostics.AddError("Unable to update private registry GPG key", err.Error()) + return + } + + result := modelFromTFEVGPGKey(key) + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) +} + +func (r *resourceTFERegistryGPGKey) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state modelTFERegistryGPGKey + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + keyID := tfe.GPGKeyID{ + RegistryName: "private", + Namespace: state.Organization.ValueString(), + KeyID: state.ID.ValueString(), + } + + tflog.Debug(ctx, "Deleting private registry GPG key") + err := r.config.Client.GPGKeys.Delete(ctx, keyID) + if err != nil { + resp.Diagnostics.AddError("Unable to delete private registry GPG key", err.Error()) + return + } +} + +func (r *resourceTFERegistryGPGKey) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + s := strings.SplitN(req.ID, "/", 2) + if len(s) != 2 { + resp.Diagnostics.AddError( + "Error importing variable", + fmt.Sprintf("Invalid variable import format: %s (expected /)", req.ID), + ) + return + } + org := s[0] + id := s[1] + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization"), org)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} diff --git a/internal/provider/resource_tfe_registry_gpg_key_test.go b/internal/provider/resource_tfe_registry_gpg_key_test.go new file mode 100644 index 000000000..ba0ab5e9d --- /dev/null +++ b/internal/provider/resource_tfe_registry_gpg_key_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccTFERegistryGPGKeyResource_basic(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFERegistryGPGKeyResourceConfig(orgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("tfe_registry_gpg_key.foobar", "id", testGPGKeyID), + resource.TestCheckResourceAttr("tfe_registry_gpg_key.foobar", "organization", orgName), + resource.TestCheckResourceAttr("tfe_registry_gpg_key.foobar", "ascii_armor", testGPGKeyArmor+"\n"), + resource.TestCheckResourceAttrSet("tfe_registry_gpg_key.foobar", "created_at"), + resource.TestCheckResourceAttrSet("tfe_registry_gpg_key.foobar", "updated_at"), + ), + }, + }, + }) +} + +func testAccTFERegistryGPGKeyResourceConfig(orgName string) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "%s" + email = "admin@tfe.local" +} + +resource "tfe_registry_gpg_key" "foobar" { + organization = tfe_organization.foobar.name + + ascii_armor = </` as the import ID. For +example: + +```shell +terraform import tfe_registry_gpg_key.example my-org-name/34365D9472D7468F +``` From 7121fd5e611b15a775c8c734d2f1821c12e27c08 Mon Sep 17 00:00:00 2001 From: Teemu Matilainen Date: Sat, 2 Dec 2023 18:06:24 +0200 Subject: [PATCH 2/8] Add `tfe_registry_gpg_key` data source Retrieves a private registry GPG key. --- CHANGELOG.md | 1 + .../provider/data_source_registry_gpg_key.go | 119 ++++++++++++++++++ .../data_source_registry_gpg_key_test.go | 46 +++++++ internal/provider/provider_next.go | 1 + website/docs/d/registry_gpg_key.html.markdown | 32 +++++ 5 files changed, 199 insertions(+) create mode 100644 internal/provider/data_source_registry_gpg_key.go create mode 100644 internal/provider/data_source_registry_gpg_key_test.go create mode 100644 website/docs/d/registry_gpg_key.html.markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 986fba354..1816e1520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ FEATURES: * **New Resource**: `r/tfe_organization_default_execution_mode` is a new resource to set the `default_execution_mode` and `default_agent_pool_id` for an organization, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' * `r/tfe_workspace`: Now uses the organization's `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode`, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' * **New Resource**: `r/tfe_registry_gpg_key` is a new resource for managing private registry GPG keys, by @tmatilai +* **New Data Source**: `d/tfe_registry_gpg_key` is a new data source to retrieve a private registry GPG key, by @tmatilai ENHANCEMENTS: * `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133) diff --git a/internal/provider/data_source_registry_gpg_key.go b/internal/provider/data_source_registry_gpg_key.go new file mode 100644 index 000000000..a86e9835c --- /dev/null +++ b/internal/provider/data_source_registry_gpg_key.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dataSourceTFERegistryGPGKey{} + _ datasource.DataSourceWithConfigure = &dataSourceTFERegistryGPGKey{} +) + +// NewRegistryGPGKeyDataSource is a helper function to simplify the provider implementation. +func NewRegistryGPGKeyDataSource() datasource.DataSource { + return &dataSourceTFERegistryGPGKey{} +} + +// dataSourceTFERegistryGPGKey is the data source implementation. +type dataSourceTFERegistryGPGKey struct { + config ConfiguredClient +} + +// Metadata returns the data source type name. +func (d *dataSourceTFERegistryGPGKey) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_registry_gpg_key" +} + +// Schema defines the schema for the data source. +func (d *dataSourceTFERegistryGPGKey) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This data source can be used to retrieve a private registry GPG key.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization. If omitted, organization must be defined in the provider config.", + Optional: true, + Computed: true, + }, + "ascii_armor": schema.StringAttribute{ + Description: "ASCII-armored representation of the GPG key.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "The time when the GPG key was created.", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: "The time when the GPG key was last updated.", + Computed: true, + }, + }, + } +} + +// Configure adds the provider configured client to the data source. +func (d *dataSourceTFERegistryGPGKey) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + + return + } + d.config = client +} + +// Read refreshes the Terraform state with the latest data. +func (d *dataSourceTFERegistryGPGKey) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data modelTFERegistryGPGKey + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var organization string + resp.Diagnostics.Append(d.config.dataOrDefaultOrganization(ctx, req.Config, &organization)...) + + if resp.Diagnostics.HasError() { + return + } + + keyID := tfe.GPGKeyID{ + RegistryName: "private", + Namespace: organization, + KeyID: data.ID.ValueString(), + } + + tflog.Debug(ctx, "Reading private registry GPG key") + key, err := d.config.Client.GPGKeys.Read(ctx, keyID) + if err != nil { + resp.Diagnostics.AddError("Unable to read private registry GPG key", err.Error()) + return + } + + data = modelFromTFEVGPGKey(key) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_source_registry_gpg_key_test.go b/internal/provider/data_source_registry_gpg_key_test.go new file mode 100644 index 000000000..f7b88a615 --- /dev/null +++ b/internal/provider/data_source_registry_gpg_key_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccTFERegistryGPGKeyDataSource_basic(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFERegistryGPGKeyDataSourceConfig(orgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.tfe_registry_gpg_key.foobar", "organization", orgName), + resource.TestCheckResourceAttrSet("data.tfe_registry_gpg_key.foobar", "id"), + resource.TestCheckResourceAttrSet("data.tfe_registry_gpg_key.foobar", "ascii_armor"), + resource.TestCheckResourceAttrSet("data.tfe_registry_gpg_key.foobar", "created_at"), + resource.TestCheckResourceAttrSet("data.tfe_registry_gpg_key.foobar", "updated_at")), + }, + }, + }) +} + +func testAccTFERegistryGPGKeyDataSourceConfig(orgName string) string { + return fmt.Sprintf(` +%s + +data "tfe_registry_gpg_key" "foobar" { + organization = tfe_organization.foobar.name + + id = tfe_registry_gpg_key.foobar.id +} +`, testAccTFERegistryGPGKeyResourceConfig(orgName)) +} diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index d79c6993b..816d60af4 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -113,6 +113,7 @@ func (p *frameworkProvider) Configure(ctx context.Context, req provider.Configur func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ + NewRegistryGPGKeyDataSource, NewSAMLSettingsDataSource, } } diff --git a/website/docs/d/registry_gpg_key.html.markdown b/website/docs/d/registry_gpg_key.html.markdown new file mode 100644 index 000000000..434bcc7af --- /dev/null +++ b/website/docs/d/registry_gpg_key.html.markdown @@ -0,0 +1,32 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_registry_gpg_key" +description: |- + Get information on a private registry GPG key. +--- + +# Data Source: tfe_registry_gpg_key + +Use this data source to get information about a private registry GPG key. + +## Example Usage + +```hcl +data "tfe_registry_gpg_key" "example" { + organization = "my-org-name" + id = "13DFECCA3B58CE4A" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Required) ID of the GPG key. +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +* `ascii_armor` - ASCII-armored representation of the GPG key. +* `created_at` - The time when the GPG key was created. +* `updated_at` - The time when the GPG key was last updated. From c951fbd22a2ba59a05fb8349824c7c6cdf485ea4 Mon Sep 17 00:00:00 2001 From: Teemu Matilainen Date: Sat, 2 Dec 2023 18:07:49 +0200 Subject: [PATCH 3/8] Add `tfe_registry_gpg_keys` data source Retrieves all private registry GPG keys of an organization. --- CHANGELOG.md | 1 + .../provider/data_source_registry_gpg_keys.go | 146 ++++++++++++++++++ .../data_source_registry_gpg_keys_test.go | 86 +++++++++++ internal/provider/provider_next.go | 1 + .../docs/d/registry_gpg_keys.html.markdown | 33 ++++ 5 files changed, 267 insertions(+) create mode 100644 internal/provider/data_source_registry_gpg_keys.go create mode 100644 internal/provider/data_source_registry_gpg_keys_test.go create mode 100644 website/docs/d/registry_gpg_keys.html.markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 1816e1520..6772601c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ FEATURES: * `r/tfe_workspace`: Now uses the organization's `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode`, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' * **New Resource**: `r/tfe_registry_gpg_key` is a new resource for managing private registry GPG keys, by @tmatilai * **New Data Source**: `d/tfe_registry_gpg_key` is a new data source to retrieve a private registry GPG key, by @tmatilai +* **New Data Source**: `d/tfe_registry_gpg_keys` is a new data source to retrieve all private registry GPG keys of an organization, by @tmatilai ENHANCEMENTS: * `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133) diff --git a/internal/provider/data_source_registry_gpg_keys.go b/internal/provider/data_source_registry_gpg_keys.go new file mode 100644 index 000000000..dc8b15f53 --- /dev/null +++ b/internal/provider/data_source_registry_gpg_keys.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &dataSourceTFERegistryGPGKeys{} + _ datasource.DataSourceWithConfigure = &dataSourceTFERegistryGPGKeys{} +) + +// NewRegistryGPGKeysDataSource is a helper function to simplify the provider implementation. +func NewRegistryGPGKeysDataSource() datasource.DataSource { + return &dataSourceTFERegistryGPGKeys{} +} + +// dataSourceTFERegistryGPGKeys is the data source implementation. +type dataSourceTFERegistryGPGKeys struct { + config ConfiguredClient +} + +// modelTFERegistryGPGKeys maps the data source schema data. +type modelTFERegistryGPGKeys struct { + ID types.String `tfsdk:"id"` + Organization types.String `tfsdk:"organization"` + Keys []modelTFERegistryGPGKey `tfsdk:"keys"` +} + +// Metadata returns the data source type name. +func (d *dataSourceTFERegistryGPGKeys) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_registry_gpg_keys" +} + +// Schema defines the schema for the data source. +func (d *dataSourceTFERegistryGPGKeys) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This data source can be used to retrieve all private registry GPG keys of an organization.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "organization": schema.StringAttribute{ + Description: "Name of the organization. If omitted, organization must be defined in the provider config.", + Optional: true, + Computed: true, + }, + "keys": schema.ListAttribute{ + Description: "List of GPG keys in the organization.", + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "organization": types.StringType, + "ascii_armor": types.StringType, + "created_at": types.StringType, + "updated_at": types.StringType, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the data source. +func (d *dataSourceTFERegistryGPGKeys) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + + return + } + d.config = client +} + +// Read refreshes the Terraform state with the latest data. +func (d *dataSourceTFERegistryGPGKeys) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data modelTFERegistryGPGKeys + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var organization string + resp.Diagnostics.Append(d.config.dataOrDefaultOrganization(ctx, req.Config, &organization)...) + + if resp.Diagnostics.HasError() { + return + } + + options := tfe.GPGKeyListOptions{ + Namespaces: []string{organization}, + } + tflog.Debug(ctx, "Listing private registry GPG keys") + keyList, err := d.config.Client.GPGKeys.ListPrivate(ctx, options) + if err != nil { + resp.Diagnostics.AddError("Unable to list private registry GPG keys", err.Error()) + return + } + + data.ID = types.StringValue(organization) + data.Organization = types.StringValue(organization) + data.Keys = []modelTFERegistryGPGKey{} + + for { + for _, key := range keyList.Items { + data.Keys = append(data.Keys, modelFromTFEVGPGKey(key)) + } + + if keyList.CurrentPage >= keyList.TotalPages { + break + } + options.PageNumber = keyList.NextPage + + tflog.Debug(ctx, "Listing private registry GPG keys") + keyList, err = d.config.Client.GPGKeys.ListPrivate(ctx, options) + if err != nil { + resp.Diagnostics.AddError("Unable to list private registry GPG keys", err.Error()) + return + } + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_source_registry_gpg_keys_test.go b/internal/provider/data_source_registry_gpg_keys_test.go new file mode 100644 index 000000000..b885be7f3 --- /dev/null +++ b/internal/provider/data_source_registry_gpg_keys_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccTFERegistryGPGKeysDataSource_basic(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFERegistryGPGKeysDataSourceConfig(orgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.tfe_registry_gpg_keys.all", "organization", orgName), + resource.TestCheckResourceAttr( + "data.tfe_registry_gpg_keys.all", "keys.#", "1"), + resource.TestCheckResourceAttrSet( + "data.tfe_registry_gpg_keys.all", "keys.0.id"), + resource.TestCheckResourceAttr( + "data.tfe_registry_gpg_keys.all", "keys.0.organization", orgName), + resource.TestCheckResourceAttrSet( + "data.tfe_registry_gpg_keys.all", "keys.0.ascii_armor"), + ), + }, + }, + }) +} + +func TestAccTFERegistryGPGKeysDataSource_basicNoKeys(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + orgName := fmt.Sprintf("tst-terraform-%d", rInt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFERegistryGPGKeysDataSourceConfig_noKeys(orgName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "data.tfe_registry_gpg_keys.all", "organization", orgName), + resource.TestCheckResourceAttr( + "data.tfe_registry_gpg_keys.all", "keys.#", "0"), + ), + }, + }, + }) +} + +func testAccTFERegistryGPGKeysDataSourceConfig(orgName string) string { + return fmt.Sprintf(` +%s + +data "tfe_registry_gpg_keys" "all" { + organization = tfe_organization.foobar.name + + depends_on = [tfe_registry_gpg_key.foobar] +} +`, testAccTFERegistryGPGKeyResourceConfig(orgName)) +} + +func testAccTFERegistryGPGKeysDataSourceConfig_noKeys(orgName string) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "%s" + email = "admin@tfe.local" +} + +data "tfe_registry_gpg_keys" "all" { + organization = tfe_organization.foobar.name +} +`, orgName) +} diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 816d60af4..847becb30 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -114,6 +114,7 @@ func (p *frameworkProvider) Configure(ctx context.Context, req provider.Configur func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewRegistryGPGKeyDataSource, + NewRegistryGPGKeysDataSource, NewSAMLSettingsDataSource, } } diff --git a/website/docs/d/registry_gpg_keys.html.markdown b/website/docs/d/registry_gpg_keys.html.markdown new file mode 100644 index 000000000..7f4276c43 --- /dev/null +++ b/website/docs/d/registry_gpg_keys.html.markdown @@ -0,0 +1,33 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_registry_gpg_keys" +description: |- + Get information on private registry GPG keys of an organization. +--- + +# Data Source: tfe_registry_gpg_key + +Use this data source to get information about all private registry GPG keys of an organization. + +## Example Usage + +```hcl +data "tfe_registry_gpg_keys" "all" { + organization = "my-org-name" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `organization` - (Optional) Name of the organization. If omitted, organization must be defined in the provider config. + +## Attributes Reference + +* `keys` - List of GPG keys in the organization. Each element contains the following attributes: + * `id` - ID of the GPG key. + * `organization` - Name of the organization. + * `ascii_armor` - ASCII-armored representation of the GPG key. + * `created_at` - The time when the GPG key was created. + * `updated_at` - The time when the GPG key was last updated. From 44a0d49605edc728640f4540b215cf3d0d00cb8a Mon Sep 17 00:00:00 2001 From: Teemu Matilainen Date: Sat, 2 Dec 2023 18:12:15 +0200 Subject: [PATCH 4/8] Add PR links to the changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6772601c1..5b762785f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,9 @@ FEATURES: * `d/tfe_registry_module`: Add `vcs_repo.tags` and `vcs_repo.branch` attributes to allow configuration of `publishing_mechanism`. Add `test_config` to support running tests on `branch`-based registry modules, by @hashimoon [1096](https://github.com/hashicorp/terraform-provider-tfe/pull/1096) * **New Resource**: `r/tfe_organization_default_execution_mode` is a new resource to set the `default_execution_mode` and `default_agent_pool_id` for an organization, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' * `r/tfe_workspace`: Now uses the organization's `default_execution_mode` and `default_agent_pool_id` as the default `execution_mode`, by @SwiftEngineer [1137](https://github.com/hashicorp/terraform-provider-tfe/pull/1137)' -* **New Resource**: `r/tfe_registry_gpg_key` is a new resource for managing private registry GPG keys, by @tmatilai -* **New Data Source**: `d/tfe_registry_gpg_key` is a new data source to retrieve a private registry GPG key, by @tmatilai -* **New Data Source**: `d/tfe_registry_gpg_keys` is a new data source to retrieve all private registry GPG keys of an organization, by @tmatilai +* **New Resource**: `r/tfe_registry_gpg_key` is a new resource for managing private registry GPG keys, by @tmatilai [1160](https://github.com/hashicorp/terraform-provider-tfe/pull/1160) +* **New Data Source**: `d/tfe_registry_gpg_key` is a new data source to retrieve a private registry GPG key, by @tmatilai [1160](https://github.com/hashicorp/terraform-provider-tfe/pull/1160) +* **New Data Source**: `d/tfe_registry_gpg_keys` is a new data source to retrieve all private registry GPG keys of an organization, by @tmatilai [1160](https://github.com/hashicorp/terraform-provider-tfe/pull/1160) ENHANCEMENTS: * `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133) From 233580a5ae3b60ed000f4501569de26a0f6ccb9d Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Wed, 6 Dec 2023 10:10:31 -0700 Subject: [PATCH 5/8] Update "Ascii" -> "ASCII" --- internal/provider/gpg_key.go | 4 ++-- internal/provider/resource_tfe_registry_gpg_key.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/provider/gpg_key.go b/internal/provider/gpg_key.go index ee25671e5..f0dc3e24f 100644 --- a/internal/provider/gpg_key.go +++ b/internal/provider/gpg_key.go @@ -15,7 +15,7 @@ import ( type modelTFERegistryGPGKey struct { ID types.String `tfsdk:"id"` Organization types.String `tfsdk:"organization"` - AsciiArmor types.String `tfsdk:"ascii_armor"` + ASCIIArmor types.String `tfsdk:"ascii_armor"` CreatedAt types.String `tfsdk:"created_at"` UpdatedAt types.String `tfsdk:"updated_at"` } @@ -26,7 +26,7 @@ func modelFromTFEVGPGKey(v *tfe.GPGKey) modelTFERegistryGPGKey { return modelTFERegistryGPGKey{ ID: types.StringValue(v.KeyID), Organization: types.StringValue(v.Namespace), - AsciiArmor: types.StringValue(v.AsciiArmor), + ASCIIArmor: types.StringValue(v.AsciiArmor), CreatedAt: types.StringValue(v.CreatedAt.Format(time.RFC3339)), UpdatedAt: types.StringValue(v.UpdatedAt.Format(time.RFC3339)), } diff --git a/internal/provider/resource_tfe_registry_gpg_key.go b/internal/provider/resource_tfe_registry_gpg_key.go index 8d1b49e0b..bcf583c0f 100644 --- a/internal/provider/resource_tfe_registry_gpg_key.go +++ b/internal/provider/resource_tfe_registry_gpg_key.go @@ -117,7 +117,7 @@ func (r *resourceTFERegistryGPGKey) Create(ctx context.Context, req resource.Cre options := tfe.GPGKeyCreateOptions{ Type: "gpg-keys", Namespace: organization, - AsciiArmor: plan.AsciiArmor.ValueString(), + AsciiArmor: plan.ASCIIArmor.ValueString(), } tflog.Debug(ctx, "Creating private registry GPG key") From dfab5d733e8f73553610f6f0e069f3effb6a155a Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Wed, 6 Dec 2023 14:33:46 -0700 Subject: [PATCH 6/8] registry_gpg_key: Add plan mod for provider org default changes --- internal/provider/provider_custom_diffs.go | 26 +++++++++++++++++-- .../provider/resource_tfe_registry_gpg_key.go | 11 +++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/provider/provider_custom_diffs.go b/internal/provider/provider_custom_diffs.go index a18ccd72a..f2cd82c69 100644 --- a/internal/provider/provider_custom_diffs.go +++ b/internal/provider/provider_custom_diffs.go @@ -3,6 +3,9 @@ package provider import ( "context" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -13,11 +16,30 @@ func customizeDiffIfProviderDefaultOrganizationChanged(c context.Context, diff * plannedOrg := diff.Get("organization").(string) if configOrg.IsNull() && config.Organization != plannedOrg { - // There is no organization configured on the resource, yet it is different from - // the state organization. We must conclude that the provider default organization changed. + // There is no organization configured on the resource, yet the provider org is different from + // the planned organization. We must conclude that the provider default organization changed. if err := diff.SetNew("organization", config.Organization); err != nil { return err } } return nil } + +func modifyPlanForDefaultOrganizationChange(ctx context.Context, providerDefaultOrg string, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.State.Raw.IsNull() { + return + } + + orgPath := path.Root("organization") + + var configOrg, plannedOrg *string + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, orgPath, &configOrg)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, orgPath, &plannedOrg)...) + + if configOrg == nil && plannedOrg != nil && providerDefaultOrg != *plannedOrg { + // There is no organization configured on the resource, yet the provider org is different from + // the planned organization value. We must conclude that the provider default organization changed. + resp.Plan.SetAttribute(ctx, orgPath, types.StringValue(providerDefaultOrg)) + resp.RequiresReplace.Append(orgPath) + } +} diff --git a/internal/provider/resource_tfe_registry_gpg_key.go b/internal/provider/resource_tfe_registry_gpg_key.go index bcf583c0f..98d8cbd44 100644 --- a/internal/provider/resource_tfe_registry_gpg_key.go +++ b/internal/provider/resource_tfe_registry_gpg_key.go @@ -37,6 +37,10 @@ func (r *resourceTFERegistryGPGKey) Metadata(ctx context.Context, req resource.M resp.TypeName = req.ProviderTypeName + "_registry_gpg_key" } +func (r *resourceTFERegistryGPGKey) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + modifyPlanForDefaultOrganizationChange(ctx, r.config.Organization, req, resp) +} + func (r *resourceTFERegistryGPGKey) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Manages a public key of the GPG key pair used to sign releases of private providers in the private registry.", @@ -57,6 +61,9 @@ func (r *resourceTFERegistryGPGKey) Schema(ctx context.Context, req resource.Sch Validators: []validator.String{ stringvalidator.LengthAtLeast(1), }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "ascii_armor": schema.StringAttribute{ Description: "ASCII-armored representation of the GPG key.", @@ -175,12 +182,10 @@ func (r *resourceTFERegistryGPGKey) Update(ctx context.Context, req resource.Upd // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if resp.Diagnostics.HasError() { - return - } // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { return } From 66550acf342385fee1fa9d07cf1d583f8ed3ce82 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 7 Dec 2023 11:58:02 -0700 Subject: [PATCH 7/8] Remove Update logic from tfe_registry_gpg_key --- .../provider/resource_tfe_registry_gpg_key.go | 39 +++---------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/internal/provider/resource_tfe_registry_gpg_key.go b/internal/provider/resource_tfe_registry_gpg_key.go index 98d8cbd44..b3b7cda49 100644 --- a/internal/provider/resource_tfe_registry_gpg_key.go +++ b/internal/provider/resource_tfe_registry_gpg_key.go @@ -177,40 +177,11 @@ func (r *resourceTFERegistryGPGKey) Read(ctx context.Context, req resource.ReadR } func (r *resourceTFERegistryGPGKey) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan modelTFERegistryGPGKey - var state modelTFERegistryGPGKey - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - if resp.Diagnostics.HasError() { - return - } - - keyID := tfe.GPGKeyID{ - RegistryName: "private", - Namespace: state.Organization.ValueString(), // The old namespace - KeyID: plan.ID.ValueString(), - } - options := tfe.GPGKeyUpdateOptions{ - Type: "gpg-keys", - Namespace: plan.Organization.ValueString(), // The new namespace - } - - tflog.Debug(ctx, "Updating private registry GPG key") - key, err := r.config.Client.GPGKeys.Update(ctx, keyID, options) - if err != nil { - resp.Diagnostics.AddError("Unable to update private registry GPG key", err.Error()) - return - } - - result := modelFromTFEVGPGKey(key) - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &result)...) + // If the resource does not support modification and should always be recreated on + // configuration value updates, the Update logic can be left empty and ensure all + // configurable schema attributes implement the resource.RequiresReplace() + // attribute plan modifier. + resp.Diagnostics.AddError("Update not supported", "The update operation is not supported on this resource. This is a bug in the provider.") } func (r *resourceTFERegistryGPGKey) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { From fbd15fd42e0b296fd7d2642fbd246925050b2e0d Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 7 Dec 2023 13:14:33 -0700 Subject: [PATCH 8/8] Add additional interface proof to resourceTFERegistryGPGKey --- internal/provider/resource_tfe_registry_gpg_key.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/resource_tfe_registry_gpg_key.go b/internal/provider/resource_tfe_registry_gpg_key.go index b3b7cda49..0a72ecbec 100644 --- a/internal/provider/resource_tfe_registry_gpg_key.go +++ b/internal/provider/resource_tfe_registry_gpg_key.go @@ -23,6 +23,7 @@ import ( var _ resource.Resource = &resourceTFERegistryGPGKey{} var _ resource.ResourceWithConfigure = &resourceTFERegistryGPGKey{} var _ resource.ResourceWithImportState = &resourceTFERegistryGPGKey{} +var _ resource.ResourceWithModifyPlan = &resourceTFERegistryGPGKey{} func NewRegistryGPGKeyResource() resource.Resource { return &resourceTFERegistryGPGKey{}