diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f9b4f13..5b762785f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +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 [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) 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/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/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/gpg_key.go b/internal/provider/gpg_key.go new file mode 100644 index 000000000..f0dc3e24f --- /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_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/provider_next.go b/internal/provider/provider_next.go index a8b964b4d..847becb30 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -113,12 +113,15 @@ 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, } } 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..0a72ecbec --- /dev/null +++ b/internal/provider/resource_tfe_registry_gpg_key.go @@ -0,0 +1,226 @@ +// 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{} +var _ resource.ResourceWithModifyPlan = &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) 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.", + 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), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "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) { + // 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) { + 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 +```