Skip to content

Commit 5fd2184

Browse files
authored
Merge pull request #1627 from hashicorp/ephemeral-agent-token
feat: add tfe_agent_token ephemeral resource
2 parents 7156f0f + 91ecb24 commit 5fd2184

File tree

6 files changed

+255
-1
lines changed

6 files changed

+255
-1
lines changed

GNUmakefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,3 @@ test-compile:
6464
go test -c $(TEST) $(TESTARGS)
6565

6666
.PHONY: build test testacc vet fmt fmtcheck errcheck test-compile sweep
67-

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ require (
7171
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
7272
github.com/cloudflare/circl v1.5.0 // indirect
7373
github.com/hashicorp/hc-install v0.9.1 // indirect
74+
github.com/hashicorp/terraform-plugin-testing v1.11.0 // indirect
7475
github.com/hashicorp/terraform-registry-address v0.2.4 // indirect
7576
github.com/kr/text v0.2.0 // indirect
7677
github.com/pmezard/go-difflib v1.0.0 // indirect
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"log"
10+
11+
tfe "github.com/hashicorp/go-tfe"
12+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
13+
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
var (
18+
_ ephemeral.EphemeralResource = &AgentTokenEphemeralResource{}
19+
)
20+
21+
func NewAgentTokenEphemeralResource() ephemeral.EphemeralResource {
22+
return &AgentTokenEphemeralResource{}
23+
}
24+
25+
type AgentTokenEphemeralResource struct {
26+
config ConfiguredClient
27+
}
28+
29+
type AgentTokenEphemeralResourceModel struct {
30+
AgentPoolID types.String `tfsdk:"agent_pool_id"`
31+
Description types.String `tfsdk:"description"`
32+
Token types.String `tfsdk:"token"`
33+
}
34+
35+
// defines a schema describing what data is available in the ephemeral resource's configuration and result data.
36+
func (e *AgentTokenEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
37+
resp.Schema = schema.Schema{
38+
Description: "This ephemeral resource can be used to retrieve an agent token without saving its value in state.",
39+
Attributes: map[string]schema.Attribute{
40+
"agent_pool_id": schema.StringAttribute{
41+
Description: `ID of the agent. If omitted, agent must be defined in the provider config.`,
42+
Required: true,
43+
},
44+
"description": schema.StringAttribute{
45+
Description: `Description of the agent token.`,
46+
Required: true,
47+
},
48+
"token": schema.StringAttribute{
49+
Description: `The generated token.`,
50+
Computed: true,
51+
Sensitive: true,
52+
},
53+
},
54+
}
55+
}
56+
57+
// Configure adds the provider configured client to the data source.
58+
func (e *AgentTokenEphemeralResource) Configure(_ context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
59+
if req.ProviderData == nil {
60+
return
61+
}
62+
63+
client, ok := req.ProviderData.(ConfiguredClient)
64+
if !ok {
65+
resp.Diagnostics.AddError(
66+
"Unexpected Ephemeral Resource Configure Type",
67+
fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData),
68+
)
69+
70+
return
71+
}
72+
e.config = client
73+
}
74+
75+
func (e *AgentTokenEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
76+
resp.TypeName = req.ProviderTypeName + "_agent_token" // tfe_agent_token
77+
}
78+
79+
// The request contains the configuration supplied to Terraform for the ephemeral resource. The response contains the ephemeral result data. The data is defined by the schema of the ephemeral resource.
80+
func (e *AgentTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
81+
var data AgentTokenEphemeralResourceModel
82+
83+
// Read Terraform config data into the model
84+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
85+
if resp.Diagnostics.HasError() {
86+
return
87+
}
88+
89+
agentPoolID := data.AgentPoolID.ValueString()
90+
description := data.Description.ValueString()
91+
92+
options := tfe.AgentTokenCreateOptions{
93+
Description: tfe.String(description),
94+
}
95+
96+
log.Printf("[DEBUG] Create new agent token for agent pool ID: %s", agentPoolID)
97+
log.Printf("[DEBUG] Create new agent token with description: %s", description)
98+
99+
result, err := e.config.Client.AgentTokens.Create(ctx, agentPoolID, options)
100+
101+
if err != nil {
102+
resp.Diagnostics.AddError("Unable to create agent token", err.Error())
103+
return
104+
}
105+
106+
data = ephemeralResourceModelFromTFEagentToken(agentPoolID, result)
107+
108+
// Save to ephemeral result data
109+
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
110+
}
111+
112+
// ephemeralResourceModelFromTFEagentToken builds a agentTokenEphemeralResourceModel struct from a
113+
// tfe.agentToken value.
114+
func ephemeralResourceModelFromTFEagentToken(id string, v *tfe.AgentToken) AgentTokenEphemeralResourceModel {
115+
return AgentTokenEphemeralResourceModel{
116+
AgentPoolID: types.StringValue(id),
117+
Description: types.StringValue(v.Description),
118+
Token: types.StringValue(v.Token),
119+
}
120+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"fmt"
8+
"regexp"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
12+
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
13+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
14+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
15+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
16+
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
17+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
18+
)
19+
20+
func TestAccagentTokenEphemeralResource_basic(t *testing.T) {
21+
tfeClient, err := getClientUsingEnv()
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
26+
org, orgCleanup := createBusinessOrganization(t, tfeClient)
27+
t.Cleanup(orgCleanup)
28+
29+
resource.Test(t, resource.TestCase{
30+
PreCheck: func() { testAccPreCheck(t) },
31+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
32+
tfversion.SkipBelow(tfversion.Version1_10_0),
33+
},
34+
ProtoV5ProviderFactories: testAccMuxedProviders,
35+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
36+
"echo": echoprovider.NewProviderServer(),
37+
},
38+
Steps: []resource.TestStep{
39+
{
40+
Config: testAccAgentTokenEphemeralResourceConfig(org.Name),
41+
ConfigStateChecks: []statecheck.StateCheck{
42+
statecheck.ExpectKnownValue("echo.this", tfjsonpath.New("data"), knownvalue.StringRegexp(regexp.MustCompile(`^[a-zA-Z0-9]+\.atlasv1\.[a-zA-Z0-9]+$`))),
43+
},
44+
},
45+
},
46+
})
47+
}
48+
49+
func testAccAgentTokenEphemeralResourceConfig(orgName string) string {
50+
return fmt.Sprintf(`
51+
resource "tfe_agent_pool" "foobar" {
52+
name = "agent-pool-test"
53+
organization = "%s"
54+
}
55+
ephemeral "tfe_agent_token" "this" {
56+
agent_pool_id = tfe_agent_pool.foobar.id
57+
description = "agent-token-test"
58+
}
59+
60+
provider "echo" {
61+
data = ephemeral.tfe_agent_token.this.token
62+
}
63+
64+
resource "echo" "this" {}
65+
`, orgName)
66+
}

internal/provider/provider_next.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99

1010
"github.com/hashicorp/terraform-plugin-framework/datasource"
11+
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
1112
"github.com/hashicorp/terraform-plugin-framework/provider"
1213
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
1314
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -22,6 +23,7 @@ type frameworkProvider struct{}
2223

2324
// Compile-time interface check
2425
var _ provider.Provider = &frameworkProvider{}
26+
var _ provider.ProviderWithEphemeralResources = &frameworkProvider{}
2527

2628
// FrameworkProviderConfig is a helper type for extracting the provider
2729
// configuration from the provider block.
@@ -109,6 +111,7 @@ func (p *frameworkProvider) Configure(ctx context.Context, req provider.Configur
109111

110112
res.DataSourceData = configuredClient
111113
res.ResourceData = configuredClient
114+
res.EphemeralResourceData = configuredClient
112115
}
113116

114117
func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
@@ -143,3 +146,9 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res
143146
NewWorkspaceRunTaskResource,
144147
}
145148
}
149+
150+
func (p *frameworkProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
151+
return []func() ephemeral.EphemeralResource{
152+
NewAgentTokenEphemeralResource,
153+
}
154+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
layout: "tfe"
3+
page_title: "Terraform Enterprise: tfe_agent_token"
4+
description: |-
5+
Generates an ephemeral agent token.
6+
---
7+
8+
# tfe_agent_token
9+
10+
Generates a new agent token as an ephemeral value.
11+
12+
Each agent pool can have multiple tokens and they can be long-lived. For that reason, this ephemeral resource does not implement the Close method, which would tear the token down after the configuration is complete.
13+
14+
Agent token strings are sensitive and only returned on creation, so making those strings ephemeral values is beneficial to avoid state exposure.
15+
16+
If you need to use this value in the future, make sure to capture the token and save it in a secure location. Any resource with write-only values can accept ephemeral resource attributes.
17+
18+
## Example Usage
19+
20+
Basic usage:
21+
22+
```hcl
23+
ephemeral "tfe_agent_token" "this" {
24+
agent_pool_id = tfe_agent_pool.foobar.id
25+
description = "my description"
26+
}
27+
```
28+
29+
## Argument Reference
30+
31+
The following arguments are supported:
32+
33+
* `agent_pool_id` - (Required) Id for the Agent Pool.
34+
* `description` - (Required) A brief description about the Agent Pool.
35+
36+
## Example Usage
37+
38+
```hcl
39+
resource "tfe_agent_pool" "foobar" {
40+
name = "agent-pool-test"
41+
organization = "my-org-name"
42+
}
43+
44+
ephemeral "tfe_agent_token" "this" {
45+
agent_pool_id = tfe_agent_pool.foobar.id
46+
description = "my description"
47+
}
48+
49+
output "my-agent-token" {
50+
value = ephemeral.tfe_agent_token.this.token
51+
description = "Token for tfe agent."
52+
ephemeral = true
53+
}
54+
```
55+
56+
## Attributes Reference
57+
58+
* `token` - The generated token.
59+

0 commit comments

Comments
 (0)