Skip to content

Commit 3d360ac

Browse files
feat: add tfe_team_token ephemeral resource (#1628)
* feat: add tfe_team_token ephemeral resource * chore: update changelog --------- Co-authored-by: Chris Trombley <chris.trombley@hashicorp.com>
1 parent 5fd2184 commit 3d360ac

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## Unreleased
2+
3+
FEATURES:
4+
5+
* **New Ephemeral Resource:** `tfe_team_token` is a new ephemeral
6+
resource for creating and managing team tokens in an organization, by
7+
@shwetamurali and @ctrombley [#1628](https://github.com/hashicorp/terraform-provider-tfe/pull/1628)
8+
19
## v.0.64.0
210

311
FEATURES:
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/go-tfe"
11+
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
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 = &TeamTokenEphemeralResource{}
19+
_ ephemeral.EphemeralResourceWithConfigure = &TeamTokenEphemeralResource{}
20+
)
21+
22+
func NewTeamTokenEphemeralResource() ephemeral.EphemeralResource {
23+
return &TeamTokenEphemeralResource{}
24+
}
25+
26+
type TeamTokenEphemeralResource struct {
27+
config ConfiguredClient
28+
}
29+
30+
type TeamTokenEphemeralResourceModel struct {
31+
TeamID types.String `tfsdk:"team_id"`
32+
Token types.String `tfsdk:"token"`
33+
ExpiredAt timetypes.RFC3339 `tfsdk:"expired_at"`
34+
}
35+
36+
func (e *TeamTokenEphemeralResource) 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 a team token without saving its value in state.",
39+
Attributes: map[string]schema.Attribute{
40+
"team_id": schema.StringAttribute{
41+
Description: `ID of the team.`,
42+
Required: true,
43+
},
44+
"token": schema.StringAttribute{
45+
Description: `The generated token.`,
46+
Computed: true,
47+
Sensitive: true,
48+
},
49+
"expired_at": schema.StringAttribute{
50+
Description: `The token's expiration date.`,
51+
Optional: true,
52+
Computed: true,
53+
CustomType: timetypes.RFC3339Type{},
54+
},
55+
},
56+
}
57+
}
58+
59+
// Configure adds the provider configured client to the data source.
60+
func (e *TeamTokenEphemeralResource) Configure(_ context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
61+
if req.ProviderData == nil {
62+
return
63+
}
64+
65+
client, ok := req.ProviderData.(ConfiguredClient)
66+
if !ok {
67+
resp.Diagnostics.AddError(
68+
"Unexpected Ephemeral Resource Configure Type",
69+
fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData),
70+
)
71+
72+
return
73+
}
74+
75+
e.config = client
76+
}
77+
78+
func (e *TeamTokenEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
79+
resp.TypeName = req.ProviderTypeName + "_team_token"
80+
}
81+
82+
func (e *TeamTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
83+
var config TeamTokenEphemeralResourceModel
84+
85+
// Read Terraform config data into the model
86+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
87+
if resp.Diagnostics.HasError() {
88+
return
89+
}
90+
91+
// Create a new options struct
92+
options := tfe.TeamTokenCreateOptions{}
93+
94+
if !config.ExpiredAt.IsNull() {
95+
expiredAt, diags := config.ExpiredAt.ValueRFC3339Time()
96+
if diags.HasError() {
97+
resp.Diagnostics.Append(diags...)
98+
return
99+
}
100+
101+
options.ExpiredAt = &expiredAt
102+
}
103+
104+
var teamID = config.TeamID.ValueString()
105+
result, err := e.config.Client.TeamTokens.CreateWithOptions(ctx, config.TeamID.ValueString(), options)
106+
if err != nil {
107+
resp.Diagnostics.AddError("Unable to read resource", err.Error())
108+
return
109+
}
110+
111+
config = ephemeralResourceModelFromTFETeamToken(teamID, result)
112+
113+
// Save to ephemeral result data
114+
resp.Diagnostics.Append(resp.Result.Set(ctx, &config)...)
115+
}
116+
117+
// ephemeralResourceModelFromTFETeamToken builds a TeamTokenEphemeralResourceModel struct from a
118+
// tfe.TeamToken value.
119+
func ephemeralResourceModelFromTFETeamToken(teamID string, v *tfe.TeamToken) TeamTokenEphemeralResourceModel {
120+
return TeamTokenEphemeralResourceModel{
121+
TeamID: types.StringValue(teamID),
122+
Token: types.StringValue(v.Token),
123+
ExpiredAt: timetypes.NewRFC3339TimeValue(v.ExpiredAt),
124+
}
125+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 TestAccTeamTokenEphemeralResource_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+
ProtoV5ProviderFactories: testAccMuxedProviders,
32+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
33+
"echo": echoprovider.NewProviderServer(),
34+
},
35+
Steps: []resource.TestStep{
36+
{
37+
Config: testAccTeamTokenEphemeralResourceConfig(org.Name),
38+
ConfigStateChecks: []statecheck.StateCheck{
39+
statecheck.ExpectKnownValue("echo.this", tfjsonpath.New("data").AtMapKey("team_id"), knownvalue.StringRegexp(regexp.MustCompile(`^team\-[a-zA-Z0-9]+$`))),
40+
},
41+
},
42+
},
43+
})
44+
}
45+
46+
func TestAccTeamTokenEphemeralResource_expiredAt(t *testing.T) {
47+
tfeClient, err := getClientUsingEnv()
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
52+
org, orgCleanup := createBusinessOrganization(t, tfeClient)
53+
t.Cleanup(orgCleanup)
54+
55+
resource.UnitTest(t, resource.TestCase{
56+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
57+
tfversion.SkipBelow(tfversion.Version1_10_0),
58+
},
59+
PreCheck: func() { testAccPreCheck(t) },
60+
ProtoV5ProviderFactories: testAccMuxedProviders,
61+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
62+
"echo": echoprovider.NewProviderServer(),
63+
},
64+
Steps: []resource.TestStep{
65+
{
66+
Config: testAccTeamTokenEphemeralResourceConfig_expiredAt(org.Name),
67+
},
68+
},
69+
})
70+
}
71+
72+
func testAccTeamTokenEphemeralResourceConfig(orgName string) string {
73+
return fmt.Sprintf(`
74+
resource "tfe_team" "this" {
75+
name = "team-test"
76+
organization = "%s"
77+
}
78+
79+
ephemeral "tfe_team_token" "this" {
80+
team_id = tfe_team.this.id
81+
}
82+
83+
provider "echo" {
84+
data = ephemeral.tfe_team_token.this
85+
}
86+
87+
resource "echo" "this" {}
88+
`, orgName)
89+
}
90+
91+
func testAccTeamTokenEphemeralResourceConfig_expiredAt(orgName string) string {
92+
return fmt.Sprintf(`
93+
resource "tfe_team" "this" {
94+
name = "team-test"
95+
organization = "%s"
96+
}
97+
98+
ephemeral "tfe_team_token" "this" {
99+
team_id = tfe_team.this.id
100+
expired_at = "2100-01-01T00:00:00Z"
101+
}
102+
provider "echo" {
103+
data = ephemeral.tfe_team_token.this
104+
}
105+
resource "echo" "this" {}
106+
`, orgName)
107+
}

internal/provider/provider_next.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,5 +150,6 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res
150150
func (p *frameworkProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
151151
return []func() ephemeral.EphemeralResource{
152152
NewAgentTokenEphemeralResource,
153+
NewTeamTokenEphemeralResource,
153154
}
154155
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
layout: "tfe"
3+
page_title: "Terraform Enterprise: Ephemeral: tfe_team_token"
4+
description: |-
5+
Generates a new team token that is guaranteed not to be written to
6+
state.
7+
---
8+
9+
# Ephemeral: tfe_team_token
10+
11+
Terraform ephemeral resource for managing a TFE team token. This
12+
resource is used to generate a new team token that is guaranteed not to
13+
be written to state. Since team tokens are singleton resources, using this ephemeral resource will replace any existing team token for a given team.
14+
15+
~> **NOTE:** Ephemeral resources are a new feature and may evolve as we continue to explore their most effective uses. [Learn more](https://developer.hashicorp.com/terraform/language/v1.10.x/resources/ephemeral).
16+
17+
## Example Usage
18+
19+
### Generate a new team token:
20+
21+
This will invalidate any existing team token.
22+
23+
```hcl
24+
resource "tfe_team" "example" {
25+
organization = "my-org-name"
26+
name = "my-team-name"
27+
}
28+
29+
ephemeral "tfe_team_token" "example" {
30+
team_id = tfe_team.example.id
31+
}
32+
```
33+
34+
## Argument Reference
35+
36+
The following arguments are required:
37+
38+
* `team_id` - (Required) ID of the team.
39+
40+
The following arguments are optional:
41+
42+
* `expired_at` - (Optional) The token's expiration date. The expiration date must be a date/time string in RFC3339
43+
format (e.g., "2024-12-31T23:59:59Z"). If no expiration date is supplied, the expiration date will default to null and
44+
never expire.
45+
46+
This ephemeral resource exports the following attributes in addition to the arguments above:
47+
48+
* `token` - The generated token. This value is sensitive and will not be stored
49+
in state.

0 commit comments

Comments
 (0)