Skip to content

Commit

Permalink
feat: add tfe_organization_token ephemeral resource
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrombley committed Mar 5, 2025
1 parent 5fd2184 commit 8518b38
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 0 deletions.
1 change: 1 addition & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ test-compile:
go test -c $(TEST) $(TESTARGS)

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

123 changes: 123 additions & 0 deletions internal/provider/ephemeral_resource_organization_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"fmt"
"time"

tfe "github.com/hashicorp/go-tfe"
"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 = &OrganizationTokenEphemeralResource{}
_ ephemeral.EphemeralResourceWithConfigure = &OrganizationTokenEphemeralResource{}
)

func NewOrganizationTokenEphemeralResource() ephemeral.EphemeralResource {
return &OrganizationTokenEphemeralResource{}
}

type OrganizationTokenEphemeralResource struct {
config ConfiguredClient
}

type OrganizationTokenEphemeralResourceModel struct {
Organization types.String `tfsdk:"organization"`
ExpiredAt types.String `tfsdk:"expired_at"`
Token types.String `tfsdk:"token"`
}

func (e *OrganizationTokenEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "This ephemeral resource can be used to retrieve an organization token without saving its value in state. Using this ephemeral resource will generate a new token each time it is used, invalidating any existing organization token.",
Attributes: map[string]schema.Attribute{
"organization": schema.StringAttribute{
Description: `Name of the organization. If omitted, organization must be defined in the provider config.`,
Optional: true,
Computed: true,
},
"expired_at": schema.StringAttribute{
Description: `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.`,
Optional: true,
},
"token": schema.StringAttribute{
Description: `The generated token.`,
Computed: true,
},
},
}
}

// Configure adds the provider configured client to the data source.
func (e *OrganizationTokenEphemeralResource) 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 *OrganizationTokenEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization_token"
}

func (e *OrganizationTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
// Read Terraform config config
var config OrganizationTokenEphemeralResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

// Get org name or default
var orgName string
resp.Diagnostics.Append(e.config.dataOrDefaultOrganization(ctx, req.Config, &orgName)...)
if resp.Diagnostics.HasError() {
return
}

// Create options
var expiredAt *time.Time
if !config.ExpiredAt.IsNull() {
parsed, err := time.Parse(time.RFC3339, config.ExpiredAt.String())
if err != nil {
resp.Diagnostics.AddError("Invalid expired_at value", err.Error())
return
}

expiredAt = &parsed
}

opts := tfe.OrganizationTokenCreateOptions{
ExpiredAt: expiredAt,
}

// Create a new token
result, err := e.config.Client.OrganizationTokens.CreateWithOptions(ctx, orgName, opts)
if err != nil {
resp.Diagnostics.AddError("Unable to create organization token", err.Error())
return
}

// Set the token in the model
config.Token = types.StringValue(result.Token)

// Write the data back to the ephemeral resource
resp.Diagnostics.Append(resp.Result.Set(ctx, &config)...)
}
106 changes: 106 additions & 0 deletions internal/provider/ephemeral_resource_organization_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"fmt"
"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 TestAccOrganizationTokenEphemeralResource_basic(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: testAccOrganizationTokenEphemeralResourceConfig_basic(org.Name),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("echo.this", tfjsonpath.New("data"), knownvalue.StringExact(org.Name)),
},
RefreshState: false,
},
},
})
}

func TestAccOrganizationTokenEphemeralResource_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: testAccOrganizationTokenEphemeralResourceConfig_expiredAt(org.Name),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("echo.this", tfjsonpath.New("data"), knownvalue.StringExact("2100-01-01T00:00:00Z")),
},
RefreshState: false,
},
},
})
}

func testAccOrganizationTokenEphemeralResourceConfig_basic(orgName string) string {
return fmt.Sprintf(`
ephemeral "tfe_organization_token" "this" {
organization = "%s"
}
provider "echo" {
data = ephemeral.tfe_organization_token.this.organization
}
resource "echo" "this" {}
`, orgName)
}

func testAccOrganizationTokenEphemeralResourceConfig_expiredAt(orgName string) string {
return fmt.Sprintf(`
ephemeral "tfe_organization_token" "this" {
organization = "%s"
expired_at = "2100-01-01T00:00:00Z"
}
provider "echo" {
data = ephemeral.tfe_organization_token.this.expired_at
}
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,
NewOrganizationTokenEphemeralResource,
}
}
44 changes: 44 additions & 0 deletions website/docs/ephemeral-resources/organization_token.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
layout: "tfe"
page_title: "Terraform Enterprise: Ephemeral: tfe_organization_token"
description: |-
Generates a new organization token that is guaranteed not to be written to
state.
---

# Ephemeral: tfe_organization_token

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

~> **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 organization token:

This will invalidate any existing organization token.

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

## Argument Reference

The following arguments are required:

* `organization` - (Required) Name of the organization. If omitted, organization must be defined in the provider config.

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.

0 comments on commit 8518b38

Please sign in to comment.