Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tfe_team_token ephemeral resource #1628

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Unreleased

FEATURES:

* **New Ephemeral Resource:** `tfe_team_token` is a new ephemeral
resource for creating and managing team tokens in an organization, by
@shwetamurali and @ctrombley [#1628](https://github.com/hashicorp/terraform-provider-tfe/pull/1628)

## v.0.64.0

FEATURES:
Expand Down
125 changes: 125 additions & 0 deletions internal/provider/ephemeral_resource_team_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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-timetypes/timetypes"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var (
_ ephemeral.EphemeralResource = &TeamTokenEphemeralResource{}
_ ephemeral.EphemeralResourceWithConfigure = &TeamTokenEphemeralResource{}
)

func NewTeamTokenEphemeralResource() ephemeral.EphemeralResource {
return &TeamTokenEphemeralResource{}
}

type TeamTokenEphemeralResource struct {
config ConfiguredClient
}

type TeamTokenEphemeralResourceModel struct {
TeamID types.String `tfsdk:"team_id"`
Token types.String `tfsdk:"token"`
ExpiredAt timetypes.RFC3339 `tfsdk:"expired_at"`
}

func (e *TeamTokenEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "This ephemeral resource can be used to retrieve a team token without saving its value in state.",
Attributes: map[string]schema.Attribute{
"team_id": schema.StringAttribute{
Description: `ID of the team.`,
Required: true,
},
"token": schema.StringAttribute{
Description: `The generated token.`,
Computed: true,
Sensitive: true,
},
"expired_at": schema.StringAttribute{
Description: `The token's expiration date.`,
Optional: true,
Computed: true,
CustomType: timetypes.RFC3339Type{},
},
},
}
}

// Configure adds the provider configured client to the data source.
func (e *TeamTokenEphemeralResource) Configure(_ context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(ConfiguredClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Ephemeral 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),
)

return
}

e.config = client
}

func (e *TeamTokenEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_team_token"
}

func (e *TeamTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var config TeamTokenEphemeralResourceModel

// Read Terraform config data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// Create a new options struct
options := tfe.TeamTokenCreateOptions{}

if !config.ExpiredAt.IsNull() {
expiredAt, diags := config.ExpiredAt.ValueRFC3339Time()
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

options.ExpiredAt = &expiredAt
}

var teamID = config.TeamID.ValueString()
result, err := e.config.Client.TeamTokens.CreateWithOptions(ctx, config.TeamID.ValueString(), options)
if err != nil {
resp.Diagnostics.AddError("Unable to read resource", err.Error())
return
}

config = ephemeralResourceModelFromTFETeamToken(teamID, result)

// Save to ephemeral result data
resp.Diagnostics.Append(resp.Result.Set(ctx, &config)...)
}

// ephemeralResourceModelFromTFETeamToken builds a TeamTokenEphemeralResourceModel struct from a
// tfe.TeamToken value.
func ephemeralResourceModelFromTFETeamToken(teamID string, v *tfe.TeamToken) TeamTokenEphemeralResourceModel {
return TeamTokenEphemeralResourceModel{
TeamID: types.StringValue(teamID),
Token: types.StringValue(v.Token),
ExpiredAt: timetypes.NewRFC3339TimeValue(v.ExpiredAt),
}
}
107 changes: 107 additions & 0 deletions internal/provider/ephemeral_resource_team_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestAccTeamTokenEphemeralResource_basic(t *testing.T) {
tfeClient, err := getClientUsingEnv()
if err != nil {
t.Fatal(err)
}

org, orgCleanup := createBusinessOrganization(t, tfeClient)
t.Cleanup(orgCleanup)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV5ProviderFactories: testAccMuxedProviders,
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"echo": echoprovider.NewProviderServer(),
},
Steps: []resource.TestStep{
{
Config: testAccTeamTokenEphemeralResourceConfig(org.Name),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("echo.this", tfjsonpath.New("data").AtMapKey("team_id"), knownvalue.StringRegexp(regexp.MustCompile(`^team\-[a-zA-Z0-9]+$`))),
},
},
},
})
}

func TestAccTeamTokenEphemeralResource_expiredAt(t *testing.T) {
tfeClient, err := getClientUsingEnv()
if err != nil {
t.Fatal(err)
}

org, orgCleanup := createBusinessOrganization(t, tfeClient)
t.Cleanup(orgCleanup)

resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_10_0),
},
PreCheck: func() { testAccPreCheck(t) },
ProtoV5ProviderFactories: testAccMuxedProviders,
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"echo": echoprovider.NewProviderServer(),
},
Steps: []resource.TestStep{
{
Config: testAccTeamTokenEphemeralResourceConfig_expiredAt(org.Name),
},
},
})
}

func testAccTeamTokenEphemeralResourceConfig(orgName string) string {
return fmt.Sprintf(`
resource "tfe_team" "this" {
name = "team-test"
organization = "%s"
}

ephemeral "tfe_team_token" "this" {
team_id = tfe_team.this.id
}

provider "echo" {
data = ephemeral.tfe_team_token.this
}

resource "echo" "this" {}
`, orgName)
}

func testAccTeamTokenEphemeralResourceConfig_expiredAt(orgName string) string {
return fmt.Sprintf(`
resource "tfe_team" "this" {
name = "team-test"
organization = "%s"
}

ephemeral "tfe_team_token" "this" {
team_id = tfe_team.this.id
expired_at = "2100-01-01T00:00:00Z"
}
provider "echo" {
data = ephemeral.tfe_team_token.this
}
resource "echo" "this" {}
`, orgName)
}
1 change: 1 addition & 0 deletions internal/provider/provider_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,6 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res
func (p *frameworkProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
NewAgentTokenEphemeralResource,
NewTeamTokenEphemeralResource,
}
}
49 changes: 49 additions & 0 deletions website/docs/ephemeral-resources/team_token.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
layout: "tfe"
page_title: "Terraform Enterprise: Ephemeral: tfe_team_token"
description: |-
Generates a new team token that is guaranteed not to be written to
state.
---

# Ephemeral: tfe_team_token

Terraform ephemeral resource for managing a TFE team token. This
resource is used to generate a new team token that is guaranteed not to
be written to state. Since team tokens are singleton resources, using this ephemeral resource will replace any existing team token for a given team.

~> **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).

## Example Usage

### Generate a new team token:

This will invalidate any existing team token.

```hcl
resource "tfe_team" "example" {
organization = "my-org-name"
name = "my-team-name"
}

ephemeral "tfe_team_token" "example" {
team_id = tfe_team.example.id
}
```

## Argument Reference

The following arguments are required:

* `team_id` - (Required) ID of the team.

The following arguments are optional:

* `expired_at` - (Optional) The token's expiration date. The expiration date must be a date/time string in RFC3339
format (e.g., "2024-12-31T23:59:59Z"). If no expiration date is supplied, the expiration date will default to null and
never expire.

This ephemeral resource exports the following attributes in addition to the arguments above:

* `token` - The generated token. This value is sensitive and will not be stored
in state.