Skip to content

Commit

Permalink
feat: add ephemeral resource (#58)
Browse files Browse the repository at this point in the history
* fix: replace deprecated golang linter exportloopref with copyloopvar

Signed-off-by: Kim Oliver Drechsel <kim@drechsel.xyz>

* feat: add ephemeral resource

Signed-off-by: Kim Oliver Drechsel <kim@drechsel.xyz>

* ci: update terraform version in provider acceptance tests

Signed-off-by: Kim Oliver Drechsel <kim@drechsel.xyz>

---------

Signed-off-by: Kim Oliver Drechsel <kim@drechsel.xyz>
  • Loading branch information
kimdre authored Nov 30, 2024
1 parent 36a4e7b commit 008ede6
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
matrix:
# list whatever Terraform versions here you would like to support
terraform:
- '1.8.*'
- '1.10.*'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ linters:
- durationcheck
- dupword
- errcheck
- exportloopref
- copyloopvar
- forcetypeassert
- forbidigo
- gci
Expand Down
33 changes: 33 additions & 0 deletions docs/ephemeral-resources/dotenv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "dotenv Ephemeral Resource - dotenv"
subcategory: ""
description: |-
Reads and provides all entries of a dotenv file.
All supported formats can be found here https://registry.terraform.io/providers/germanbrew/dotenv/latest/docs#supported-formats.
-> If you only need a specific value you can use the get_by_key https://registry.terraform.io/providers/germanbrew/dotenv/latest/docs/functions/get_by_key provider function.
Ephemeral resources are not stored in the state.
---

# dotenv (Ephemeral Resource)

Reads and provides all entries of a dotenv file.

All supported formats can be found [here](https://registry.terraform.io/providers/germanbrew/dotenv/latest/docs#supported-formats).

-> If you only need a specific value you can use the [`get_by_key`](https://registry.terraform.io/providers/germanbrew/dotenv/latest/docs/functions/get_by_key) provider function.

Ephemeral resources are not stored in the state.



<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `filename` (String) `Default: .env` Path to the dotenv file

### Read-Only

- `entries` (Map of String) Key-Value entries of the dotenv file.
112 changes: 112 additions & 0 deletions internal/provider/file_ephemeral_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package provider

import (
"context"
"fmt"

"github.com/germanbrew/terraform-provider-dotenv/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ ephemeral.EphemeralResource = &fileDotEnvEphemeralResource{}

func NewFileDotEnvEphemeralResource() ephemeral.EphemeralResource {
return &fileDotEnvEphemeralResource{}
}

// fileDotEnvEphemeralResource defines the data source implementation.
type fileDotEnvEphemeralResource struct{}

// fileDotEnvEphemeralResourceModel describes the data source data model.
type fileDotEnvEphemeralResourceModel struct {
Filename types.String `tfsdk:"filename"`
Entries types.Map `tfsdk:"entries"`
}

func (d *fileDotEnvEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName // + "_file"
}

func (d *fileDotEnvEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Reads and provides all entries of a dotenv file.\n\n" +
"All supported formats can be found [here](https://registry.terraform.io/providers/germanbrew/dotenv/latest/docs#supported-formats).\n\n" +
"-> If you only need a specific value you can use the " +
"[`get_by_key`](https://registry.terraform.io/providers/germanbrew/dotenv/latest/docs/functions/get_by_key) provider function.\n\n" +
"Ephemeral resources are not stored in the state. ",

Attributes: map[string]schema.Attribute{
"filename": schema.StringAttribute{
MarkdownDescription: "`Default: .env` Path to the dotenv file",
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"entries": schema.MapAttribute{
MarkdownDescription: "Key-Value entries of the dotenv file.",
Computed: true,
ElementType: types.StringType,
},
},
}
}

func (d *fileDotEnvEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
}

func (d *fileDotEnvEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var data fileDotEnvEphemeralResourceModel

// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

filename := data.Filename.ValueString()

if filename == "" {
filename = ".env"
tflog.Info(ctx, "No file name specified, so the default is used: "+filename)
}

parsedEntries, err := utils.ParseDotEnvFile(filename)
if err != nil {
resp.Diagnostics.AddError("Parse Error", fmt.Sprintf("Parsing contents of file %s failed: %s", filename, err))

return
}

entries := make(map[string]attr.Value, len(parsedEntries))
for key, value := range parsedEntries {
entries[key] = types.StringValue(value)

if err != nil {
resp.Diagnostics.AddError("Conversion Error", fmt.Sprintf("Failed to convert key %s value %s: %s", key, value, err))

return
}
}

tflog.Debug(ctx, "Parsing the file was successful")

data.Filename = types.StringValue(filename)
data.Entries, _ = types.MapValue(types.StringType, entries)

// Save data into ephemeral result data
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}
128 changes: 128 additions & 0 deletions internal/provider/file_ephemeral_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package provider

import (
"fmt"
"regexp"
"testing"

"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 TestAccEphemeralResource_DotEnvFile_KnownKey(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_10_0),
},
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithEcho,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccExampleEphemeralResourceConfig,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("EXAMPLE_STRING"), knownvalue.StringExact("Example v@lue!")),
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("EXAMPLE_INT"), knownvalue.StringExact("100")),
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("EXAMPLE_FLOAT"), knownvalue.StringExact("1.23")),
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("SOME_VAR"), knownvalue.StringExact("someval")),
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("BAR"), knownvalue.StringExact("BAZ")),
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("FOO"), knownvalue.StringExact("BAR")),
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("YAML_FOO"), knownvalue.StringExact("bar")),
},
},
},
})
}

func TestAccEphemeralResource_DotEnvFile_UnknownKey(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_10_0),
},
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithEcho,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccExampleEphemeralResourceConfig,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("echo.env", tfjsonpath.New("data").AtMapKey("unknown"), knownvalue.StringExact("invalid")),
},
ExpectError: regexp.MustCompile(`path not found: specified key unknown not found in map at data.unknown`),
},
},
})
}

func TestAccEphemeralResource_DotEnvFile_UnknownFile(t *testing.T) {
resource.Test(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_10_0),
},
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithEcho,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccUnknownEphemeralResourceConfig,
ExpectError: regexp.MustCompile(fmt.Sprintf("%s: %s", "testdata/unknown.env", ErrFileNotFound)),
},
},
})
}

func TestAccEphemeralResource_DotEnvFile_InvalidLine(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithEcho,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccInvalidEphemeralResourceConfig,
ExpectError: regexp.MustCompile(fmt.Sprintf("%s: %s", ErrInvalidLine, "this is invalid")),
},
},
})
}

// lintignore:AT004
const testAccExampleEphemeralResourceConfig = `
ephemeral "dotenv" "test" {
filename = "./testdata/test.env"
}
provider "echo" {
data = ephemeral.dotenv.test.entries
}
resource "echo" "env" {}
`

// lintignore:AT004
const testAccUnknownEphemeralResourceConfig = `
ephemeral "dotenv" "test" {
filename = "./testdata/unknown.env"
}
provider "echo" {
data = ephemeral.dotenv.test.entries
}
resource "echo" "env" {}
`

// lintignore:AT004
const testAccInvalidEphemeralResourceConfig = `
ephemeral "dotenv" "test" {
filename = "./testdata/invalid.env"
}
provider "echo" {
data = ephemeral.dotenv.test.entries
}
resource "echo" "env" {}
`
7 changes: 7 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
Expand Down Expand Up @@ -65,6 +66,12 @@ func (p *DotenvProvider) Resources(ctx context.Context) []func() resource.Resour
return []func() resource.Resource{}
}

func (p *DotenvProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
NewFileDotEnvEphemeralResource,
}
}

func (p *DotenvProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewFileDotEnvDataSource,
Expand Down
7 changes: 7 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/echoprovider"
)

// testAccProtoV6ProviderFactories are used to instantiate a provider during
Expand All @@ -20,6 +21,12 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe
"dotenv": providerserver.NewProtocol6WithError(New("test")()),
}

//nolint:gochecknoglobals
var testAccProtoV6ProviderFactoriesWithEcho = map[string]func() (tfprotov6.ProviderServer, error){
"dotenv": providerserver.NewProtocol6WithError(New("test")()),
"echo": echoprovider.NewProviderServer(),
}

func testAccPreCheck(t *testing.T) {
// You can add code here to run prior to any test case execution, for example assertions
// about the appropriate environment variables being set are common to see in a pre-check
Expand Down

0 comments on commit 008ede6

Please sign in to comment.