diff --git a/GNUmakefile b/GNUmakefile index 8d76c0197..4e0f073bc 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -64,3 +64,4 @@ test-compile: go test -c $(TEST) $(TESTARGS) .PHONY: build test testacc vet fmt fmtcheck errcheck test-compile sweep + diff --git a/internal/provider/ephemeral_resource_organization_token.go b/internal/provider/ephemeral_resource_organization_token.go new file mode 100644 index 000000000..7f6c6c522 --- /dev/null +++ b/internal/provider/ephemeral_resource_organization_token.go @@ -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)...) +} diff --git a/internal/provider/ephemeral_resource_organization_token_test.go b/internal/provider/ephemeral_resource_organization_token_test.go new file mode 100644 index 000000000..be470f6a0 --- /dev/null +++ b/internal/provider/ephemeral_resource_organization_token_test.go @@ -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) +} diff --git a/internal/provider/provider_next.go b/internal/provider/provider_next.go index 1b2663869..d195a1798 100644 --- a/internal/provider/provider_next.go +++ b/internal/provider/provider_next.go @@ -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, } } diff --git a/website/docs/ephemeral-resources/organization_token.html.markdown b/website/docs/ephemeral-resources/organization_token.html.markdown new file mode 100644 index 000000000..4bece0d3a --- /dev/null +++ b/website/docs/ephemeral-resources/organization_token.html.markdown @@ -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.