Skip to content

Commit 2ac26bd

Browse files
tmatilaibrandonc
authored andcommitted
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.
1 parent 632457d commit 2ac26bd

File tree

8 files changed

+477
-7
lines changed

8 files changed

+477
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ FEATURES:
1313
* `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)
1414
* **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)'
1515
* `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)'
16+
* **New Resource**: `r/tfe_registry_gpg_key` is a new resource for managing private registry GPG keys, by @tmatilai
1617

1718
ENHANCEMENTS:
1819
* `d/tfe_organization`: Make `name` argument optional if configured for the provider, by @tmatilai [1133](https://github.com/hashicorp/terraform-provider-tfe/pull/1133)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
)
13+
14+
// AttrGettable is a small enabler for helper functions that need to read one
15+
// attribute of a Configuration, Plan, or State.
16+
type AttrGettable interface {
17+
GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics
18+
}
19+
20+
// dataOrDefaultOrganization returns the value of the "organization" attribute
21+
// from the Config/Plan/State data, defaulting to the provier configuration.
22+
// If neither is set, an error is returned.
23+
func (c *ConfiguredClient) dataOrDefaultOrganization(ctx context.Context, data AttrGettable, target *string) diag.Diagnostics {
24+
schemaPath := path.Root("organization")
25+
26+
var organization types.String
27+
diags := data.GetAttribute(ctx, schemaPath, &organization)
28+
if diags.HasError() {
29+
return diags
30+
}
31+
32+
if !organization.IsNull() && !organization.IsUnknown() {
33+
*target = organization.ValueString()
34+
} else if c.Organization == "" {
35+
diags.AddAttributeError(schemaPath, "No organization was specified on the resource or provider", "")
36+
} else {
37+
*target = c.Organization
38+
}
39+
40+
return diags
41+
}

internal/provider/gpg_key.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"time"
8+
9+
"github.com/hashicorp/go-tfe"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
)
12+
13+
// modelTFERegistryGPGKey maps the resource or data source schema data to a
14+
// struct.
15+
type modelTFERegistryGPGKey struct {
16+
ID types.String `tfsdk:"id"`
17+
Organization types.String `tfsdk:"organization"`
18+
AsciiArmor types.String `tfsdk:"ascii_armor"`
19+
CreatedAt types.String `tfsdk:"created_at"`
20+
UpdatedAt types.String `tfsdk:"updated_at"`
21+
}
22+
23+
// modelFromTFEVGPGKey builds a modelTFERegistryGPGKey struct from a
24+
// tfe.GPGKey value.
25+
func modelFromTFEVGPGKey(v *tfe.GPGKey) modelTFERegistryGPGKey {
26+
return modelTFERegistryGPGKey{
27+
ID: types.StringValue(v.KeyID),
28+
Organization: types.StringValue(v.Namespace),
29+
AsciiArmor: types.StringValue(v.AsciiArmor),
30+
CreatedAt: types.StringValue(v.CreatedAt.Format(time.RFC3339)),
31+
UpdatedAt: types.StringValue(v.UpdatedAt.Format(time.RFC3339)),
32+
}
33+
}

internal/provider/provider_next.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource
119119

120120
func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Resource {
121121
return []func() resource.Resource{
122+
NewRegistryGPGKeyResource,
122123
NewResourceVariable,
123124
NewSAMLSettingsResource,
124125
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/go-tfe"
12+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
14+
"github.com/hashicorp/terraform-plugin-framework/resource"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
18+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
19+
"github.com/hashicorp/terraform-plugin-log/tflog"
20+
)
21+
22+
// Ensure provider defined types fully satisfy framework interfaces.
23+
var _ resource.Resource = &resourceTFERegistryGPGKey{}
24+
var _ resource.ResourceWithConfigure = &resourceTFERegistryGPGKey{}
25+
var _ resource.ResourceWithImportState = &resourceTFERegistryGPGKey{}
26+
27+
func NewRegistryGPGKeyResource() resource.Resource {
28+
return &resourceTFERegistryGPGKey{}
29+
}
30+
31+
// resourceTFERegistryGPGKey implements the tfe_registry_gpg_key resource type
32+
type resourceTFERegistryGPGKey struct {
33+
config ConfiguredClient
34+
}
35+
36+
func (r *resourceTFERegistryGPGKey) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
37+
resp.TypeName = req.ProviderTypeName + "_registry_gpg_key"
38+
}
39+
40+
func (r *resourceTFERegistryGPGKey) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
41+
resp.Schema = schema.Schema{
42+
Description: "Manages a public key of the GPG key pair used to sign releases of private providers in the private registry.",
43+
Version: 1,
44+
45+
Attributes: map[string]schema.Attribute{
46+
"id": schema.StringAttribute{
47+
Description: "ID of the GPG key.",
48+
Computed: true,
49+
PlanModifiers: []planmodifier.String{
50+
stringplanmodifier.UseStateForUnknown(),
51+
},
52+
},
53+
"organization": schema.StringAttribute{
54+
Description: "Name of the organization. If omitted, organization must be defined in the provider config.",
55+
Optional: true,
56+
Computed: true,
57+
Validators: []validator.String{
58+
stringvalidator.LengthAtLeast(1),
59+
},
60+
},
61+
"ascii_armor": schema.StringAttribute{
62+
Description: "ASCII-armored representation of the GPG key.",
63+
Required: true,
64+
PlanModifiers: []planmodifier.String{
65+
stringplanmodifier.RequiresReplace(),
66+
},
67+
},
68+
"created_at": schema.StringAttribute{
69+
Description: "The time when the GPG key was created.",
70+
Computed: true,
71+
PlanModifiers: []planmodifier.String{
72+
stringplanmodifier.UseStateForUnknown(),
73+
},
74+
},
75+
"updated_at": schema.StringAttribute{
76+
Description: "The time when the GPG key was last updated.",
77+
Computed: true,
78+
},
79+
},
80+
}
81+
}
82+
83+
// Configure implements resource.ResourceWithConfigure
84+
func (r *resourceTFERegistryGPGKey) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
85+
// Prevent panic if the provider has not been configured.
86+
if req.ProviderData == nil {
87+
return
88+
}
89+
90+
client, ok := req.ProviderData.(ConfiguredClient)
91+
if !ok {
92+
resp.Diagnostics.AddError(
93+
"Unexpected resource Configure type",
94+
fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData),
95+
)
96+
}
97+
r.config = client
98+
}
99+
100+
func (r *resourceTFERegistryGPGKey) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
101+
var plan modelTFERegistryGPGKey
102+
103+
// Read Terraform plan data into the model
104+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
105+
106+
if resp.Diagnostics.HasError() {
107+
return
108+
}
109+
110+
var organization string
111+
resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Plan, &organization)...)
112+
113+
if resp.Diagnostics.HasError() {
114+
return
115+
}
116+
117+
options := tfe.GPGKeyCreateOptions{
118+
Type: "gpg-keys",
119+
Namespace: organization,
120+
AsciiArmor: plan.AsciiArmor.ValueString(),
121+
}
122+
123+
tflog.Debug(ctx, "Creating private registry GPG key")
124+
key, err := r.config.Client.GPGKeys.Create(ctx, "private", options)
125+
if err != nil {
126+
resp.Diagnostics.AddError("Unable to create private registry GPG key", err.Error())
127+
return
128+
}
129+
130+
result := modelFromTFEVGPGKey(key)
131+
132+
// Save data into Terraform state
133+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
134+
}
135+
136+
func (r *resourceTFERegistryGPGKey) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
137+
var state modelTFERegistryGPGKey
138+
139+
// Read Terraform prior state data into the model
140+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
141+
142+
if resp.Diagnostics.HasError() {
143+
return
144+
}
145+
146+
var organization string
147+
resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.State, &organization)...)
148+
149+
if resp.Diagnostics.HasError() {
150+
return
151+
}
152+
153+
keyID := tfe.GPGKeyID{
154+
RegistryName: "private",
155+
Namespace: organization,
156+
KeyID: state.ID.ValueString(),
157+
}
158+
159+
tflog.Debug(ctx, "Reading private registry GPG key")
160+
key, err := r.config.Client.GPGKeys.Read(ctx, keyID)
161+
if err != nil {
162+
resp.Diagnostics.AddError("Unable to read private registry GPG key", err.Error())
163+
return
164+
}
165+
166+
result := modelFromTFEVGPGKey(key)
167+
168+
// Save updated data into Terraform state
169+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
170+
}
171+
172+
func (r *resourceTFERegistryGPGKey) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
173+
var plan modelTFERegistryGPGKey
174+
var state modelTFERegistryGPGKey
175+
176+
// Read Terraform plan data into the model
177+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
178+
if resp.Diagnostics.HasError() {
179+
return
180+
}
181+
182+
// Read Terraform prior state data into the model
183+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
184+
if resp.Diagnostics.HasError() {
185+
return
186+
}
187+
188+
keyID := tfe.GPGKeyID{
189+
RegistryName: "private",
190+
Namespace: state.Organization.ValueString(), // The old namespace
191+
KeyID: plan.ID.ValueString(),
192+
}
193+
options := tfe.GPGKeyUpdateOptions{
194+
Type: "gpg-keys",
195+
Namespace: plan.Organization.ValueString(), // The new namespace
196+
}
197+
198+
tflog.Debug(ctx, "Updating private registry GPG key")
199+
key, err := r.config.Client.GPGKeys.Update(ctx, keyID, options)
200+
if err != nil {
201+
resp.Diagnostics.AddError("Unable to update private registry GPG key", err.Error())
202+
return
203+
}
204+
205+
result := modelFromTFEVGPGKey(key)
206+
207+
// Save updated data into Terraform state
208+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
209+
}
210+
211+
func (r *resourceTFERegistryGPGKey) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
212+
var state modelTFERegistryGPGKey
213+
214+
// Read Terraform prior state data into the model
215+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
216+
217+
if resp.Diagnostics.HasError() {
218+
return
219+
}
220+
221+
keyID := tfe.GPGKeyID{
222+
RegistryName: "private",
223+
Namespace: state.Organization.ValueString(),
224+
KeyID: state.ID.ValueString(),
225+
}
226+
227+
tflog.Debug(ctx, "Deleting private registry GPG key")
228+
err := r.config.Client.GPGKeys.Delete(ctx, keyID)
229+
if err != nil {
230+
resp.Diagnostics.AddError("Unable to delete private registry GPG key", err.Error())
231+
return
232+
}
233+
}
234+
235+
func (r *resourceTFERegistryGPGKey) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
236+
s := strings.SplitN(req.ID, "/", 2)
237+
if len(s) != 2 {
238+
resp.Diagnostics.AddError(
239+
"Error importing variable",
240+
fmt.Sprintf("Invalid variable import format: %s (expected <ORGANIZATION>/<KEY ID>)", req.ID),
241+
)
242+
return
243+
}
244+
org := s[0]
245+
id := s[1]
246+
247+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization"), org)...)
248+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
249+
}

0 commit comments

Comments
 (0)