diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ac0188..35fb0d9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -49,8 +49,8 @@ jobs: - name: Validate Examples (skip provider directory) run: | for dir in examples/*/; do - if [[ "$(basename "$dir")" == "provider" ]]; then - echo "Skipping provider directory (requires published provider)" + if [[ "$(basename "$dir")" == "provider" || "$(basename "$dir")" == "ephemeral-credentials" ]]; then + echo "Skipping $(basename "$dir") directory (requires published provider)" continue fi echo "Validating $dir" diff --git a/docs/index.md b/docs/index.md index 5a862d6..e798f06 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,6 +33,7 @@ provider "eon" { client_id = var.eon_client_id client_secret = var.eon_client_secret project_id = var.eon_project_id + token = var.eon_token } ``` @@ -96,3 +97,4 @@ provider "eon" { - `client_secret` (String, Sensitive) Eon API client secret for authentication. Can also be set with the `EON_CLIENT_SECRET` environment variable. - `endpoint` (String) Eon API base URL in the format `https://.console.eon.io` (no trailing slash). Can also be set with the `EON_ENDPOINT` environment variable. - `project_id` (String) Eon project ID. Can also be set with the `EON_PROJECT_ID` environment variable. +- `token` (String, Sensitive) Eon API token for authentication. Can also be set with the `EON_TOKEN` environment variable. diff --git a/docs/resources/backup_policy.md b/docs/resources/backup_policy.md index 0dabb15..a9e5c0f 100644 --- a/docs/resources/backup_policy.md +++ b/docs/resources/backup_policy.md @@ -577,11 +577,11 @@ Required: Optional: -- `daily_config` (Attributes) Daily configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--daily_config)) -- `weekly_config` (Attributes) Weekly configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--weekly_config)) -- `monthly_config` (Attributes) Monthly configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--monthly_config)) - `annually_config` (Attributes) Annually configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--annually_config)) +- `daily_config` (Attributes) Daily configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--daily_config)) - `interval_config` (Attributes) Interval configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--interval_config)) +- `monthly_config` (Attributes) Monthly configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--monthly_config)) +- `weekly_config` (Attributes) Weekly configuration (see [below for nested schema](#nestedatt--backup_plan--standard_plan--backup_schedules--schedule_config--weekly_config)) ### Nested Schema for `backup_plan.standard_plan.backup_schedules.schedule_config.annually_config` diff --git a/examples/ephemeral-credentials/provider.tf b/examples/ephemeral-credentials/provider.tf new file mode 100644 index 0000000..84d4f63 --- /dev/null +++ b/examples/ephemeral-credentials/provider.tf @@ -0,0 +1,20 @@ +variable "client_id" { + type = string + default = "Eon API client ID" + sensitive = true + ephemeral = true +} + +variable "client_secret" { + type = string + default = "Eon API client secret" + sensitive = true + ephemeral = true +} + +provider "eon" { + endpoint = "https://your-eon-endpoint.co" + client_id = var.client_id + client_secret = var.client_secret + project_id = "Eon project ID" +} \ No newline at end of file diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 3731ab5..ffaaa0c 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -11,4 +11,5 @@ provider "eon" { client_id = var.eon_client_id client_secret = var.eon_client_secret project_id = var.eon_project_id + token = var.eon_token } \ No newline at end of file diff --git a/examples/provider/variables.tf b/examples/provider/variables.tf index 2df9f17..e89e3c2 100644 --- a/examples/provider/variables.tf +++ b/examples/provider/variables.tf @@ -19,4 +19,10 @@ variable "eon_client_secret" { variable "eon_project_id" { description = "Eon project ID" type = string -} \ No newline at end of file +} + +variable "eon_token" { + description = "Eon API token" + type = string + sensitive = true +} \ No newline at end of file diff --git a/internal/client/client.go b/internal/client/client.go index 2048909..6b12375 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -21,6 +21,28 @@ type EonClient struct { endpoint string } +func NewEonClientWithToken(endpoint, projectID, token string) (*EonClient, error) { + config := externalEonSdkAPI.NewConfiguration() + config.Servers = []externalEonSdkAPI.ServerConfiguration{ + { + URL: fmt.Sprintf("%s/api", endpoint), + }, + } + + client := &EonClient{ + client: externalEonSdkAPI.NewAPIClient(config), + ProjectID: projectID, + authToken: token, + endpoint: endpoint, + } + + client.tokenExpiry = time.Now().Add(time.Duration(60 * 60 * 24 * time.Second)) // 1 day + + client.client.GetConfig().DefaultHeader["Authorization"] = "Bearer " + token + + return client, nil +} + // NewEonClient creates a new Eon API client with the provided configuration func NewEonClient(endpoint, clientID, clientSecret, projectID string) (*EonClient, error) { config := externalEonSdkAPI.NewConfiguration() diff --git a/internal/provider/basic_test.go b/internal/provider/basic_test.go index cb7eadd..2f0aeb4 100644 --- a/internal/provider/basic_test.go +++ b/internal/provider/basic_test.go @@ -40,6 +40,7 @@ func TestEonProvider_Schema(t *testing.T) { assert.Contains(t, resp.Schema.Attributes, "client_id") assert.Contains(t, resp.Schema.Attributes, "client_secret") assert.Contains(t, resp.Schema.Attributes, "project_id") + assert.Contains(t, resp.Schema.Attributes, "token") } // TestEonProvider_Resources tests the provider resources registration @@ -83,12 +84,14 @@ func TestEonProviderModel(t *testing.T) { ClientId: types.StringValue("test-client-id"), ClientSecret: types.StringValue("test-client-secret"), ProjectId: types.StringValue("test-project-id"), + Token: types.StringValue("test-token"), } assert.Equal(t, "https://test.eon.io", model.Endpoint.ValueString()) assert.Equal(t, "test-client-id", model.ClientId.ValueString()) assert.Equal(t, "test-client-secret", model.ClientSecret.ValueString()) assert.Equal(t, "test-project-id", model.ProjectId.ValueString()) + assert.Equal(t, "test-token", model.Token.ValueString()) } // TestEonProvider_StringValues tests string value handling @@ -210,11 +213,11 @@ func TestEonProvider_ProviderSchema(t *testing.T) { require.NotNil(t, resp.Schema) assert.False(t, resp.Diagnostics.HasError()) - // Test that we have exactly 4 attributes - assert.Equal(t, 4, len(resp.Schema.Attributes)) + // Test that we have exactly 5 attributes + assert.Equal(t, 5, len(resp.Schema.Attributes)) // Test attribute names - expectedAttributes := []string{"endpoint", "client_id", "client_secret", "project_id"} + expectedAttributes := []string{"endpoint", "client_id", "client_secret", "project_id", "token"} for _, attr := range expectedAttributes { assert.Contains(t, resp.Schema.Attributes, attr) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b1f8c64..daa1fb0 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -30,6 +30,7 @@ type EonProviderModel struct { ClientId types.String `tfsdk:"client_id"` ClientSecret types.String `tfsdk:"client_secret"` ProjectId types.String `tfsdk:"project_id"` + Token types.String `tfsdk:"token"` } // New creates a new provider instance. @@ -68,6 +69,11 @@ func (p *EonProvider) Schema(ctx context.Context, req provider.SchemaRequest, re MarkdownDescription: "Eon project ID. Can also be set with the `EON_PROJECT_ID` environment variable.", Optional: true, }, + "token": schema.StringAttribute{ + MarkdownDescription: "Eon API token for authentication. Can also be set with the `EON_TOKEN` environment variable.", + Optional: true, + Sensitive: true, + }, }, } } @@ -85,6 +91,7 @@ func (p *EonProvider) Configure(ctx context.Context, req provider.ConfigureReque clientId := os.Getenv("EON_CLIENT_ID") clientSecret := os.Getenv("EON_CLIENT_SECRET") projectId := os.Getenv("EON_PROJECT_ID") + token := os.Getenv("EON_TOKEN") if !data.Endpoint.IsNull() { endpoint = data.Endpoint.ValueString() @@ -102,6 +109,10 @@ func (p *EonProvider) Configure(ctx context.Context, req provider.ConfigureReque projectId = data.ProjectId.ValueString() } + if !data.Token.IsNull() { + token = data.Token.ValueString() + } + // Validate required fields if endpoint == "" { resp.Diagnostics.AddAttributeError( @@ -111,20 +122,22 @@ func (p *EonProvider) Configure(ctx context.Context, req provider.ConfigureReque ) } - if clientId == "" { - resp.Diagnostics.AddAttributeError( - path.Root("client_id"), - "Missing Eon Client ID", - "The provider requires a client ID. Set the client_id value in the configuration or use the `EON_CLIENT_ID` environment variable.", - ) - } + if token == "" { + if clientId == "" { + resp.Diagnostics.AddAttributeError( + path.Root("client_id"), + "Missing Eon Client ID", + "The provider requires either a token or a client ID. Set the client_id value in the configuration or use the `EON_CLIENT_ID` environment variable.", + ) + } - if clientSecret == "" { - resp.Diagnostics.AddAttributeError( - path.Root("client_secret"), - "Missing Eon Client Secret", - "The provider requires a client secret. Set the client_secret value in the configuration or use the `EON_CLIENT_SECRET` environment variable.", - ) + if clientSecret == "" { + resp.Diagnostics.AddAttributeError( + path.Root("client_secret"), + "Missing Eon Client Secret", + "The provider requires either a token or a client secret. Set the client_secret value in the configuration or use the `EON_CLIENT_SECRET` environment variable.", + ) + } } if projectId == "" { @@ -140,7 +153,13 @@ func (p *EonProvider) Configure(ctx context.Context, req provider.ConfigureReque } // Create Eon client - eonClient, err := client.NewEonClient(endpoint, clientId, clientSecret, projectId) + var eonClient *client.EonClient + var err error + if token == "" { + eonClient, err = client.NewEonClient(endpoint, clientId, clientSecret, projectId) + } else { + eonClient, err = client.NewEonClientWithToken(endpoint, projectId, token) + } if err != nil { resp.Diagnostics.AddError( "Unable to Create Eon API Client", diff --git a/internal/provider/provider_token_test.go b/internal/provider/provider_token_test.go new file mode 100644 index 0000000..02784b3 --- /dev/null +++ b/internal/provider/provider_token_test.go @@ -0,0 +1,143 @@ +package provider + +import ( + "context" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestEonProvider_TokenFromConfig tests that token is properly read from provider config +func TestEonProvider_TokenFromConfig(t *testing.T) { + // Create a config with token + model := EonProviderModel{ + Endpoint: types.StringValue("https://test.eon.io"), + ProjectId: types.StringValue("test-project-id"), + Token: types.StringValue("test-token-value"), + } + + assert.Equal(t, "test-token-value", model.Token.ValueString()) + assert.False(t, model.Token.IsNull()) +} + +// TestEonProvider_TokenFromEnvironment tests that token is read from EON_TOKEN env var +func TestEonProvider_TokenFromEnvironment(t *testing.T) { + // Set environment variable + originalToken := os.Getenv("EON_TOKEN") + defer os.Setenv("EON_TOKEN", originalToken) + + os.Setenv("EON_TOKEN", "env-token-value") + + // Verify we can read it + token := os.Getenv("EON_TOKEN") + assert.Equal(t, "env-token-value", token) +} + +// TestEonProvider_TokenInSchema tests that token attribute is properly defined in schema +func TestEonProvider_TokenInSchema(t *testing.T) { + p := &EonProvider{} + + req := provider.SchemaRequest{} + resp := &provider.SchemaResponse{} + + p.Schema(context.Background(), req, resp) + + require.NotNil(t, resp.Schema) + assert.Contains(t, resp.Schema.Attributes, "token") + + tokenAttr := resp.Schema.Attributes["token"] + assert.NotNil(t, tokenAttr) +} + +// TestEonProvider_TokenSensitivity tests that token is marked as sensitive +func TestEonProvider_TokenSensitivity(t *testing.T) { + p := &EonProvider{} + + req := provider.SchemaRequest{} + resp := &provider.SchemaResponse{} + + p.Schema(context.Background(), req, resp) + + require.NotNil(t, resp.Schema) + require.Contains(t, resp.Schema.Attributes, "token") + + // The token attribute should exist + // In terraform-plugin-framework, sensitive is a property of StringAttribute + // We're just verifying it exists and is accessible + assert.NotNil(t, resp.Schema.Attributes["token"]) +} + +// TestEonProvider_AllAuthenticationMethods tests different authentication methods +func TestEonProvider_AllAuthenticationMethods(t *testing.T) { + testCases := []struct { + name string + endpoint string + clientId string + clientSecret string + projectId string + token string + description string + }{ + { + name: "token_only", + endpoint: "https://test.eon.io", + projectId: "project-123", + token: "token-value", + description: "Authentication with token only", + }, + { + name: "client_credentials_only", + endpoint: "https://test.eon.io", + clientId: "client-id", + clientSecret: "client-secret", + projectId: "project-123", + description: "Authentication with client credentials only", + }, + { + name: "token_preferred_when_both", + endpoint: "https://test.eon.io", + clientId: "client-id", + clientSecret: "client-secret", + projectId: "project-123", + token: "token-value", + description: "Token should be preferred when both methods provided", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + model := EonProviderModel{ + Endpoint: types.StringValue(tc.endpoint), + ProjectId: types.StringValue(tc.projectId), + } + + if tc.clientId != "" { + model.ClientId = types.StringValue(tc.clientId) + } + if tc.clientSecret != "" { + model.ClientSecret = types.StringValue(tc.clientSecret) + } + if tc.token != "" { + model.Token = types.StringValue(tc.token) + } + + // Verify the model was constructed correctly + assert.Equal(t, tc.endpoint, model.Endpoint.ValueString()) + assert.Equal(t, tc.projectId, model.ProjectId.ValueString()) + + if tc.token != "" { + assert.Equal(t, tc.token, model.Token.ValueString()) + } + if tc.clientId != "" { + assert.Equal(t, tc.clientId, model.ClientId.ValueString()) + } + if tc.clientSecret != "" { + assert.Equal(t, tc.clientSecret, model.ClientSecret.ValueString()) + } + }) + } +}