diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b66e9fe..a83a142c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,8 +75,8 @@ jobs: - "ubuntu-latest" - "windows-latest" terraform: - - "1.6.*" - - "1.0.*" + - "1.7.*" + - "1.4.*" runs-on: ${{ matrix.os }} steps: - name: Set up Go diff --git a/client/shared_environment_variable_list.go b/client/shared_environment_variable_list.go new file mode 100644 index 00000000..984f3a2b --- /dev/null +++ b/client/shared_environment_variable_list.go @@ -0,0 +1,32 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +func (c *Client) ListSharedEnvironmentVariables(ctx context.Context, teamID string) ([]SharedEnvironmentVariableResponse, error) { + url := fmt.Sprintf("%s/v1/env/all", c.baseURL) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Trace(ctx, "listing shared environment variables", map[string]interface{}{ + "url": url, + }) + res := struct { + Data []SharedEnvironmentVariableResponse `json:"data"` + }{} + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &res) + for _, v := range res.Data { + v.TeamID = c.teamID(teamID) + } + return res.Data, err +} diff --git a/client/shared_environment_variable_update.go b/client/shared_environment_variable_update.go index f6bb73ce..693cf32f 100644 --- a/client/shared_environment_variable_update.go +++ b/client/shared_environment_variable_update.go @@ -8,7 +8,6 @@ import ( ) type UpdateSharedEnvironmentVariableRequest struct { - Key string `json:"key"` Value string `json:"value"` Type string `json:"type"` ProjectIDs []string `json:"projectId"` diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index c7dbec3c..fc252626 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -66,6 +66,7 @@ Read-Only: - `git_branch` (String) The git branch of the environment variable. - `id` (String) The ID of the environment variable - `key` (String) The name of the environment variable. +- `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. Note that the value will be `null` for sensitive environment variables. - `target` (Set of String) The environments that the environment variable should be present on. Valid targets are either `production`, `preview`, or `development`. - `value` (String) The value of the environment variable. diff --git a/docs/resources/project.md b/docs/resources/project.md index dabfb28e..a9989382 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -89,6 +89,7 @@ Required: Optional: - `git_branch` (String) The git branch of the Environment Variable. +- `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. Read-Only: diff --git a/docs/resources/project_environment_variable.md b/docs/resources/project_environment_variable.md index 417cd874..7a0fb088 100644 --- a/docs/resources/project_environment_variable.md +++ b/docs/resources/project_environment_variable.md @@ -51,6 +51,16 @@ resource "vercel_project_environment_variable" "example_git_branch" { target = ["preview"] git_branch = "staging" } + +# A sensitive environment variable that will be created +# for this project for the "production" environment. +resource "vercel_project_environment_variable" "example_sensitive" { + project_id = vercel_project.example.id + key = "foo" + value = "bar-production" + target = ["production"] + sensitive = true +} ``` @@ -66,6 +76,7 @@ resource "vercel_project_environment_variable" "example_git_branch" { ### Optional - `git_branch` (String) The git branch of the Environment Variable. +- `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. - `team_id` (String) The ID of the Vercel team.Required when configuring a team resource if a default team has not been set in the provider. ### Read-Only @@ -81,6 +92,8 @@ Import is supported using the following syntax: # the provider, simply use the project_id and environment variable id. # - project_id can be found in the project `settings` tab in the Vercel UI. # - environment variable id is hard to find, but can be taken from the network tab, inside developer tools, on the project page. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. terraform import vercel_project_environment_variable.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/FdT2e1E5Of6Cihmt # Alternatively, you can import via the team_id, project_id and @@ -88,5 +101,7 @@ terraform import vercel_project_environment_variable.example prj_xxxxxxxxxxxxxxx # - team_id can be found in the team `settings` tab in the Vercel UI. # - project_id can be found in the project `settings` tab in the Vercel UI. # - environment variable id is hard to find, but can be taken from the network tab, inside developer tools, on the project page. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. terraform import vercel_project_environment_variable.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/FdT2e1E5Of6Cihmt ``` diff --git a/docs/resources/shared_environment_variable.md b/docs/resources/shared_environment_variable.md index 1d3e5537..c571aa6d 100644 --- a/docs/resources/shared_environment_variable.md +++ b/docs/resources/shared_environment_variable.md @@ -52,6 +52,7 @@ resource "vercel_shared_environment_variable" "example" { ### Optional +- `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. - `team_id` (String) The ID of the Vercel team. Shared environment variables require a team. ### Read-Only @@ -66,5 +67,7 @@ Import is supported using the following syntax: # You can import via the team_id and environment variable id. # - team_id can be found in the team `settings` tab in the Vercel UI. # - environment variable id is hard to find, but can be taken from the network tab, inside developer tools, on the shared environment variable page. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. terraform import vercel_shared_environment_variable.example team_xxxxxxxxxxxxxxxxxxxxxxxx/env_yyyyyyyyyyyyy ``` diff --git a/examples/resources/vercel_project_environment_variable/import.sh b/examples/resources/vercel_project_environment_variable/import.sh index 2452b79d..d7f26c63 100644 --- a/examples/resources/vercel_project_environment_variable/import.sh +++ b/examples/resources/vercel_project_environment_variable/import.sh @@ -2,6 +2,8 @@ # the provider, simply use the project_id and environment variable id. # - project_id can be found in the project `settings` tab in the Vercel UI. # - environment variable id is hard to find, but can be taken from the network tab, inside developer tools, on the project page. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. terraform import vercel_project_environment_variable.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/FdT2e1E5Of6Cihmt # Alternatively, you can import via the team_id, project_id and @@ -9,4 +11,6 @@ terraform import vercel_project_environment_variable.example prj_xxxxxxxxxxxxxxx # - team_id can be found in the team `settings` tab in the Vercel UI. # - project_id can be found in the project `settings` tab in the Vercel UI. # - environment variable id is hard to find, but can be taken from the network tab, inside developer tools, on the project page. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. terraform import vercel_project_environment_variable.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/FdT2e1E5Of6Cihmt diff --git a/examples/resources/vercel_project_environment_variable/resource.tf b/examples/resources/vercel_project_environment_variable/resource.tf index bb85fa1d..a6e6a1c9 100644 --- a/examples/resources/vercel_project_environment_variable/resource.tf +++ b/examples/resources/vercel_project_environment_variable/resource.tf @@ -25,3 +25,13 @@ resource "vercel_project_environment_variable" "example_git_branch" { target = ["preview"] git_branch = "staging" } + +# A sensitive environment variable that will be created +# for this project for the "production" environment. +resource "vercel_project_environment_variable" "example_sensitive" { + project_id = vercel_project.example.id + key = "foo" + value = "bar-production" + target = ["production"] + sensitive = true +} \ No newline at end of file diff --git a/examples/resources/vercel_shared_environment_variable/import.sh b/examples/resources/vercel_shared_environment_variable/import.sh index 5eb4aa09..0dcf59fd 100644 --- a/examples/resources/vercel_shared_environment_variable/import.sh +++ b/examples/resources/vercel_shared_environment_variable/import.sh @@ -1,4 +1,6 @@ # You can import via the team_id and environment variable id. # - team_id can be found in the team `settings` tab in the Vercel UI. # - environment variable id is hard to find, but can be taken from the network tab, inside developer tools, on the shared environment variable page. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. terraform import vercel_shared_environment_variable.example team_xxxxxxxxxxxxxxxxxxxxxxxx/env_yyyyyyyyyyyyy diff --git a/sweep/main.go b/sweep/main.go index 690ee8e3..e1697d43 100644 --- a/sweep/main.go +++ b/sweep/main.go @@ -31,16 +31,36 @@ func main() { if err != nil { panic(err) } - err = deleteAllProjects(ctx, c, "") + err = deleteAllDNSRecords(ctx, c, domain, teamID) if err != nil { panic(err) } - err = deleteAllDNSRecords(ctx, c, domain, "") + err = deleteAllSharedEnvironmentVariables(ctx, c, teamID) if err != nil { panic(err) } } +func deleteAllSharedEnvironmentVariables(ctx context.Context, c *client.Client, teamID string) error { + sharedEnvironmentVariables, err := c.ListSharedEnvironmentVariables(ctx, teamID) + if err != nil { + return fmt.Errorf("error listing shared environment variables: %w", err) + } + for _, d := range sharedEnvironmentVariables { + if !strings.HasPrefix(d.Key, "test_acc") { + // Don't delete actual shared environment variables - only testing ones + continue + } + + err = c.DeleteSharedEnvironmentVariable(ctx, teamID, d.ID) + if err != nil { + return fmt.Errorf("error deleting shared env var %s: %w", d.Key, err) + } + } + + return nil +} + func deleteAllDNSRecords(ctx context.Context, c *client.Client, domain, teamID string) error { dnsRecords, err := c.ListDNSRecords(ctx, domain, teamID) if err != nil { diff --git a/vercel/contains.go b/vercel/contains.go new file mode 100644 index 00000000..3fe39aeb --- /dev/null +++ b/vercel/contains.go @@ -0,0 +1,10 @@ +package vercel + +func contains(items []string, i string) bool { + for _, j := range items { + if j == i { + return true + } + } + return false +} diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index ff92a180..9aa12213 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -118,6 +118,10 @@ For more detailed information, please see the [Vercel documentation](https://ver Description: "The git branch of the environment variable.", Computed: true, }, + "sensitive": schema.BoolAttribute{ + Description: "Whether the Environment Variable is sensitive or not. Note that the value will be `null` for sensitive environment variables.", + Computed: true, + }, }, }, }, @@ -237,7 +241,14 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest return } - result := convertResponseToProjectDataSource(out, nullProject) + result, err := convertResponseToProjectDataSource(ctx, out, nullProject) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), diff --git a/vercel/data_source_project_model.go b/vercel/data_source_project_model.go index 6e2d780f..c8bce558 100644 --- a/vercel/data_source_project_model.go +++ b/vercel/data_source_project_model.go @@ -1,6 +1,8 @@ package vercel import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/vercel/terraform-provider-vercel/client" ) @@ -26,8 +28,11 @@ type ProjectDataSource struct { TrustedIps *TrustedIps `tfsdk:"trusted_ips"` } -func convertResponseToProjectDataSource(response client.ProjectResponse, plan Project) ProjectDataSource { - project := convertResponseToProject(response, plan) +func convertResponseToProjectDataSource(ctx context.Context, response client.ProjectResponse, plan Project) (ProjectDataSource, error) { + project, err := convertResponseToProject(ctx, response, plan) + if err != nil { + return ProjectDataSource{}, err + } var pp *PasswordProtection if project.PasswordProtection != nil { @@ -53,5 +58,5 @@ func convertResponseToProjectDataSource(response client.ProjectResponse, plan Pr VercelAuthentication: project.VercelAuthentication, PasswordProtection: pp, TrustedIps: project.TrustedIps, - } + }, nil } diff --git a/vercel/resource_project.go b/vercel/resource_project.go index bb1d79f1..804f7333 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -139,6 +139,11 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Description: "The ID of the Environment Variable.", Computed: true, }, + "sensitive": schema.BoolAttribute{ + Description: "Whether the Environment Variable is sensitive or not.", + Optional: true, + Computed: true, + }, }, }, }, @@ -325,7 +330,14 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - result := convertResponseToProject(out, plan) + result, err := convertResponseToProject(ctx, out, plan) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "created project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -346,7 +358,14 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - result = convertResponseToProject(out, plan) + result, err = convertResponseToProject(ctx, out, plan) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "updated newly created project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -397,7 +416,14 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - result = convertResponseToProject(out, plan) + result, err = convertResponseToProject(ctx, out, plan) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "updated project production branch", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -437,7 +463,14 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } - result := convertResponseToProject(out, state) + result, err := convertResponseToProject(ctx, out, state) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -629,7 +662,14 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest } } - result := convertResponseToProject(out, plan) + result, err := convertResponseToProject(ctx, out, plan) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "updated project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -712,7 +752,14 @@ func (r *projectResource) ImportState(ctx context.Context, req resource.ImportSt return } - result := convertResponseToProject(out, nullProject) + result, err := convertResponseToProject(ctx, out, nullProject) + if err != nil { + resp.Diagnostics.AddError( + "Error converting project response to model", + "Could not create project, unexpected error: "+err.Error(), + ) + return + } tflog.Trace(ctx, "imported project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index 604c07c1..731a5899 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -103,6 +104,12 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown()}, Computed: true, }, + "sensitive": schema.BoolAttribute{ + Description: "Whether the Environment Variable is sensitive or not.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}, + }, }, } } @@ -135,7 +142,7 @@ func (r *projectEnvironmentVariableResource) Create(ctx context.Context, req res return } - result := convertResponseToProjectEnvironmentVariable(response, plan.ProjectID) + result := convertResponseToProjectEnvironmentVariable(response, plan.ProjectID, plan.Value) tflog.Trace(ctx, "created project environment variable", map[string]interface{}{ "id": result.ID.ValueString(), @@ -178,7 +185,7 @@ func (r *projectEnvironmentVariableResource) Read(ctx context.Context, req resou return } - result := convertResponseToProjectEnvironmentVariable(out, state.ProjectID) + result := convertResponseToProjectEnvironmentVariable(out, state.ProjectID, state.Value) tflog.Trace(ctx, "read project environment variable", map[string]interface{}{ "id": result.ID.ValueString(), "team_id": result.TeamID.ValueString(), @@ -210,7 +217,7 @@ func (r *projectEnvironmentVariableResource) Update(ctx context.Context, req res return } - result := convertResponseToProjectEnvironmentVariable(response, plan.ProjectID) + result := convertResponseToProjectEnvironmentVariable(response, plan.ProjectID, plan.Value) tflog.Trace(ctx, "updated project environment variable", map[string]interface{}{ "id": result.ID.ValueString(), @@ -296,7 +303,7 @@ func (r *projectEnvironmentVariableResource) ImportState(ctx context.Context, re return } - result := convertResponseToProjectEnvironmentVariable(out, types.StringValue(projectID)) + result := convertResponseToProjectEnvironmentVariable(out, types.StringValue(projectID), types.StringNull()) tflog.Trace(ctx, "imported project environment variable", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ProjectID.ValueString(), diff --git a/vercel/resource_project_environment_variable_model.go b/vercel/resource_project_environment_variable_model.go index a7bd6588..dfa1cb8e 100644 --- a/vercel/resource_project_environment_variable_model.go +++ b/vercel/resource_project_environment_variable_model.go @@ -14,6 +14,7 @@ type ProjectEnvironmentVariable struct { TeamID types.String `tfsdk:"team_id"` ProjectID types.String `tfsdk:"project_id"` ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` } func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest() client.CreateEnvironmentVariableRequest { @@ -21,13 +22,21 @@ func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest() client for _, t := range e.Target { target = append(target, t.ValueString()) } + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + return client.CreateEnvironmentVariableRequest{ EnvironmentVariable: client.EnvironmentVariableRequest{ Key: e.Key.ValueString(), Value: e.Value.ValueString(), Target: target, GitBranch: toStrPointer(e.GitBranch), - Type: "encrypted", + Type: envVariableType, }, ProjectID: e.ProjectID.ValueString(), TeamID: e.TeamID.ValueString(), @@ -39,12 +48,21 @@ func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest() client for _, t := range e.Target { target = append(target, t.ValueString()) } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + return client.UpdateEnvironmentVariableRequest{ Key: e.Key.ValueString(), Value: e.Value.ValueString(), Target: target, GitBranch: toStrPointer(e.GitBranch), - Type: "encrypted", + Type: envVariableType, ProjectID: e.ProjectID.ValueString(), TeamID: e.TeamID.ValueString(), EnvID: e.ID.ValueString(), @@ -54,19 +72,25 @@ func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest() client // convertResponseToProjectEnvironmentVariable is used to populate terraform state based on an API response. // Where possible, values from the API response are used to populate state. If not possible, // values from plan are used. -func convertResponseToProjectEnvironmentVariable(response client.EnvironmentVariable, projectID types.String) ProjectEnvironmentVariable { +func convertResponseToProjectEnvironmentVariable(response client.EnvironmentVariable, projectID types.String, v types.String) ProjectEnvironmentVariable { target := []types.String{} for _, t := range response.Target { target = append(target, types.StringValue(t)) } + value := types.StringValue(response.Value) + if response.Type == "sensitive" { + value = v + } + return ProjectEnvironmentVariable{ Target: target, GitBranch: fromStringPointer(response.GitBranch), Key: types.StringValue(response.Key), - Value: types.StringValue(response.Value), + Value: value, TeamID: toTeamID(response.TeamID), ProjectID: projectID, ID: types.StringValue(response.ID), + Sensitive: types.BoolValue(response.Type == "sensitive"), } } diff --git a/vercel/resource_project_environment_variable_test.go b/vercel/resource_project_environment_variable_test.go index 915e3515..2959a734 100644 --- a/vercel/resource_project_environment_variable_test.go +++ b/vercel/resource_project_environment_variable_test.go @@ -72,6 +72,12 @@ func TestAcc_ProjectEnvironmentVariables(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_environment_variable.example_git_branch", "value", "bar-staging"), resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.example_git_branch", "target.*", "preview"), resource.TestCheckResourceAttr("vercel_project_environment_variable.example_git_branch", "git_branch", "production"), + + testAccProjectEnvironmentVariableExists("vercel_project_environment_variable.example_sensitive", testTeam()), + resource.TestCheckResourceAttr("vercel_project_environment_variable.example_sensitive", "key", "foo_sensitive"), + resource.TestCheckResourceAttr("vercel_project_environment_variable.example_sensitive", "value", "bar-sensitive"), + resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.example_sensitive", "target.*", "production"), + resource.TestCheckResourceAttr("vercel_project_environment_variable.example_sensitive", "sensitive", "true"), ), }, { @@ -88,6 +94,12 @@ func TestAcc_ProjectEnvironmentVariables(t *testing.T) { resource.TestCheckResourceAttr("vercel_project_environment_variable.example_git_branch", "value", "bar-staging"), resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.example_git_branch", "target.*", "preview"), resource.TestCheckResourceAttr("vercel_project_environment_variable.example_git_branch", "git_branch", "test"), + + testAccProjectEnvironmentVariableExists("vercel_project_environment_variable.example_sensitive", testTeam()), + resource.TestCheckResourceAttr("vercel_project_environment_variable.example_sensitive", "key", "foo_sensitive_updated"), + resource.TestCheckResourceAttr("vercel_project_environment_variable.example_sensitive", "value", "bar-sensitive-updated"), + resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.example_sensitive", "target.*", "production"), + resource.TestCheckResourceAttr("vercel_project_environment_variable.example_sensitive", "sensitive", "true"), ), }, { @@ -158,6 +170,15 @@ resource "vercel_project_environment_variable" "example_git_branch" { target = ["preview"] git_branch = "production" } + +resource "vercel_project_environment_variable" "example_sensitive" { + project_id = vercel_project.example.id + %[3]s + key = "foo_sensitive" + value = "bar-sensitive" + target = ["production"] + sensitive = true +} `, projectName, testGithubRepo(), teamIDConfig()) } @@ -189,6 +210,15 @@ resource "vercel_project_environment_variable" "example_git_branch" { target = ["preview"] git_branch = "test" } + +resource "vercel_project_environment_variable" "example_sensitive" { + project_id = vercel_project.example.id + %[3]s + key = "foo_sensitive_updated" + value = "bar-sensitive-updated" + target = ["production"] + sensitive = true +} `, projectName, testGithubRepo(), teamIDConfig()) } diff --git a/vercel/resource_project_model.go b/vercel/resource_project_model.go index ae4d8669..f45e0e2e 100644 --- a/vercel/resource_project_model.go +++ b/vercel/resource_project_model.go @@ -63,12 +63,20 @@ func parseEnvironment(vars []EnvironmentItem) []client.EnvironmentVariable { target = append(target, t.ValueString()) } + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + out = append(out, client.EnvironmentVariable{ Key: e.Key.ValueString(), Value: e.Value.ValueString(), Target: target, GitBranch: toStrPointer(e.GitBranch), - Type: "encrypted", + Type: envVariableType, ID: e.ID.ValueString(), }) } @@ -122,6 +130,7 @@ type EnvironmentItem struct { Key types.String `tfsdk:"key"` Value types.String `tfsdk:"value"` ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` } func (e *EnvironmentItem) toEnvironmentVariableRequest() client.EnvironmentVariableRequest { @@ -129,12 +138,21 @@ func (e *EnvironmentItem) toEnvironmentVariableRequest() client.EnvironmentVaria for _, t := range e.Target { target = append(target, t.ValueString()) } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + return client.EnvironmentVariableRequest{ Key: e.Key.ValueString(), Value: e.Value.ValueString(), Target: target, GitBranch: toStrPointer(e.GitBranch), - Type: "encrypted", + Type: envVariableType, } } @@ -303,10 +321,24 @@ var envVariableElemType = types.ObjectType{ }, "git_branch": types.StringType, "id": types.StringType, + "sensitive": types.BoolType, }, } -func convertResponseToProject(response client.ProjectResponse, plan Project) Project { +func hasSameTarget(p EnvironmentItem, target []string) bool { + if len(p.Target) != len(target) { + return false + } + for _, t := range p.Target { + v := t.ValueString() + if !contains(target, v) { + return false + } + } + return true +} + +func convertResponseToProject(ctx context.Context, response client.ProjectResponse, plan Project) (Project, error) { fields := plan.coercedFields() var gr *GitRepository @@ -333,7 +365,7 @@ func convertResponseToProject(response client.ProjectResponse, plan Project) Pro } } - var va *VercelAuthentication = &VercelAuthentication{ + var va = &VercelAuthentication{ DeploymentType: types.StringValue("none"), } if response.VercelAuthentication != nil { @@ -364,6 +396,21 @@ func convertResponseToProject(response client.ProjectResponse, plan Project) Pro for _, t := range e.Target { target = append(target, types.StringValue(t)) } + value := types.StringValue(e.Value) + if e.Type == "sensitive" { + value = types.StringNull() + environment, err := plan.environment(ctx) + if err != nil { + return Project{}, fmt.Errorf("error reading project environment variables: %s", err) + } + for _, p := range environment { + if p.Sensitive.ValueBool() && p.Key.ValueString() == e.Key && hasSameTarget(p, e.Target) { + value = p.Value + break + } + } + } + env = append(env, types.ObjectValueMust( map[string]attr.Type{ "key": types.StringType, @@ -373,13 +420,15 @@ func convertResponseToProject(response client.ProjectResponse, plan Project) Pro }, "git_branch": types.StringType, "id": types.StringType, + "sensitive": types.BoolType, }, map[string]attr.Value{ "key": types.StringValue(e.Key), - "value": types.StringValue(e.Value), + "value": value, "target": types.SetValueMust(types.StringType, target), "git_branch": fromStringPointer(e.GitBranch), "id": types.StringValue(e.ID), + "sensitive": types.BoolValue(e.Type == "sensitive"), }, )) } @@ -422,5 +471,5 @@ func convertResponseToProject(response client.ProjectResponse, plan Project) Pro TrustedIps: tip, ProtectionBypassForAutomation: protectionBypass, ProtectionBypassForAutomationSecret: protectionBypassSecret, - } + }, nil } diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index 11a7a3da..70b82b68 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -354,6 +354,12 @@ resource "vercel_project" "test" { key = "bar" value = "baz" target = ["production"] + }, + { + key = "sensitive_thing" + value = "bar_updated" + target = ["production"] + sensitive = true } ] } @@ -594,6 +600,12 @@ resource "vercel_project" "test" { key = "oh_no" value = "bar" target = ["production"] + }, + { + key = "sensitive_thing" + value = "bar" + target = ["production"] + sensitive = true } ] } diff --git a/vercel/resource_shared_environment_variable.go b/vercel/resource_shared_environment_variable.go index 5ea50417..6c045753 100644 --- a/vercel/resource_shared_environment_variable.go +++ b/vercel/resource_shared_environment_variable.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -96,6 +97,12 @@ For more detailed information, please see the [Vercel documentation](https://ver PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown()}, Computed: true, }, + "sensitive": schema.BoolAttribute{ + Description: "Whether the Environment Variable is sensitive or not.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}, + }, }, } } @@ -119,7 +126,7 @@ func (r *sharedEnvironmentVariableResource) Create(ctx context.Context, req reso return } - result := convertResponseToSharedEnvironmentVariable(response) + result := convertResponseToSharedEnvironmentVariable(response, plan.Value) tflog.Trace(ctx, "created shared environment variable", map[string]interface{}{ "id": result.ID.ValueString(), @@ -160,7 +167,7 @@ func (r *sharedEnvironmentVariableResource) Read(ctx context.Context, req resour return } - result := convertResponseToSharedEnvironmentVariable(out) + result := convertResponseToSharedEnvironmentVariable(out, state.Value) tflog.Trace(ctx, "read shared environment variable", map[string]interface{}{ "id": result.ID.ValueString(), "team_id": result.TeamID.ValueString(), @@ -191,7 +198,7 @@ func (r *sharedEnvironmentVariableResource) Update(ctx context.Context, req reso return } - result := convertResponseToSharedEnvironmentVariable(response) + result := convertResponseToSharedEnvironmentVariable(response, plan.Value) tflog.Trace(ctx, "updated project environment variable", map[string]interface{}{ "id": result.ID.ValueString(), @@ -271,7 +278,7 @@ func (r *sharedEnvironmentVariableResource) ImportState(ctx context.Context, req return } - result := convertResponseToSharedEnvironmentVariable(out) + result := convertResponseToSharedEnvironmentVariable(out, types.StringNull()) tflog.Trace(ctx, "imported shared environment variable", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "env_id": result.ID.ValueString(), diff --git a/vercel/resource_shared_environment_variable_model.go b/vercel/resource_shared_environment_variable_model.go index 8462e768..83bb83b7 100644 --- a/vercel/resource_shared_environment_variable_model.go +++ b/vercel/resource_shared_environment_variable_model.go @@ -13,6 +13,7 @@ type SharedEnvironmentVariable struct { TeamID types.String `tfsdk:"team_id"` ProjectIDs []types.String `tfsdk:"project_ids"` ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` } func (e *SharedEnvironmentVariable) toCreateSharedEnvironmentVariableRequest() client.CreateSharedEnvironmentVariableRequest { @@ -24,10 +25,19 @@ func (e *SharedEnvironmentVariable) toCreateSharedEnvironmentVariableRequest() c for _, t := range e.ProjectIDs { projectIDs = append(projectIDs, t.ValueString()) } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + return client.CreateSharedEnvironmentVariableRequest{ EnvironmentVariable: client.SharedEnvironmentVariableRequest{ Target: target, - Type: "encrypted", + Type: envVariableType, ProjectIDs: projectIDs, EnvironmentVariables: []client.SharedEnvVarRequest{ { @@ -49,11 +59,17 @@ func (e *SharedEnvironmentVariable) toUpdateSharedEnvironmentVariableRequest() c for _, t := range e.ProjectIDs { projectIDs = append(projectIDs, t.ValueString()) } + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } return client.UpdateSharedEnvironmentVariableRequest{ - Key: e.Key.ValueString(), Value: e.Value.ValueString(), Target: target, - Type: "encrypted", + Type: envVariableType, TeamID: e.TeamID.ValueString(), EnvID: e.ID.ValueString(), ProjectIDs: projectIDs, @@ -63,7 +79,7 @@ func (e *SharedEnvironmentVariable) toUpdateSharedEnvironmentVariableRequest() c // convertResponseToSharedEnvironmentVariable is used to populate terraform state based on an API response. // Where possible, values from the API response are used to populate state. If not possible, // values from plan are used. -func convertResponseToSharedEnvironmentVariable(response client.SharedEnvironmentVariableResponse) SharedEnvironmentVariable { +func convertResponseToSharedEnvironmentVariable(response client.SharedEnvironmentVariableResponse, v types.String) SharedEnvironmentVariable { target := []types.String{} for _, t := range response.Target { target = append(target, types.StringValue(t)) @@ -74,12 +90,18 @@ func convertResponseToSharedEnvironmentVariable(response client.SharedEnvironmen project_ids = append(project_ids, types.StringValue(t)) } + value := types.StringValue(response.Value) + if response.Type == "sensitive" { + value = v + } + return SharedEnvironmentVariable{ Target: target, Key: types.StringValue(response.Key), - Value: types.StringValue(response.Value), + Value: value, ProjectIDs: project_ids, TeamID: toTeamID(response.TeamID), ID: types.StringValue(response.ID), + Sensitive: types.BoolValue(response.Type == "sensitive"), } } diff --git a/vercel/resource_shared_environment_variable_test.go b/vercel/resource_shared_environment_variable_test.go index 08dbd647..2b022f20 100644 --- a/vercel/resource_shared_environment_variable_test.go +++ b/vercel/resource_shared_environment_variable_test.go @@ -26,26 +26,6 @@ func testAccSharedEnvironmentVariableExists(n, teamID string) resource.TestCheck } } -func testAccSharedEnvironmentVariableDoesNotExist(n, teamID string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("no ID is set") - } - - _, err := testClient().GetSharedEnvironmentVariable(context.TODO(), teamID, rs.Primary.ID) - - if err != nil { - return nil - } - return fmt.Errorf("expected an error, but got none") - } -} - func TestAcc_SharedEnvironmentVariables(t *testing.T) { nameSuffix := acctest.RandString(16) resource.Test(t, resource.TestCase{ @@ -59,19 +39,27 @@ func TestAcc_SharedEnvironmentVariables(t *testing.T) { Config: testAccSharedEnvironmentVariablesConfig(nameSuffix), Check: resource.ComposeAggregateTestCheckFunc( testAccSharedEnvironmentVariableExists("vercel_shared_environment_variable.example", testTeam()), - resource.TestCheckResourceAttr("vercel_shared_environment_variable.example", "key", "foo"), + resource.TestCheckResourceAttr("vercel_shared_environment_variable.example", "key", fmt.Sprintf("test_acc_foo_%s", nameSuffix)), resource.TestCheckResourceAttr("vercel_shared_environment_variable.example", "value", "bar"), resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.example", "target.*", "production"), + + resource.TestCheckResourceAttr("vercel_shared_environment_variable.sensitive_example", "key", fmt.Sprintf("test_acc_foo_sensitive_%s", nameSuffix)), + resource.TestCheckResourceAttr("vercel_shared_environment_variable.sensitive_example", "value", "bar"), + resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.sensitive_example", "target.*", "production"), ), }, { Config: testAccSharedEnvironmentVariablesConfigUpdated(nameSuffix), Check: resource.ComposeAggregateTestCheckFunc( testAccSharedEnvironmentVariableExists("vercel_shared_environment_variable.example", testTeam()), - resource.TestCheckResourceAttr("vercel_shared_environment_variable.example", "key", "foo"), + resource.TestCheckResourceAttr("vercel_shared_environment_variable.example", "key", fmt.Sprintf("test_acc_foo_%s", nameSuffix)), resource.TestCheckResourceAttr("vercel_shared_environment_variable.example", "value", "updated-bar"), resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.example", "target.*", "development"), resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.example", "target.*", "preview"), + + resource.TestCheckResourceAttr("vercel_shared_environment_variable.sensitive_example", "key", fmt.Sprintf("test_acc_foo_sensitive_%s", nameSuffix)), + resource.TestCheckResourceAttr("vercel_shared_environment_variable.sensitive_example", "value", "bar-updated"), + resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.sensitive_example", "target.*", "production"), ), }, { @@ -82,9 +70,6 @@ func TestAcc_SharedEnvironmentVariables(t *testing.T) { }, { Config: testAccSharedEnvironmentVariablesConfigDeleted(nameSuffix), - Check: resource.ComposeAggregateTestCheckFunc( - testAccSharedEnvironmentVariableDoesNotExist("vercel_project.example", testTeam()), - ), }, }, }) @@ -114,8 +99,19 @@ resource "vercel_project" "example" { resource "vercel_shared_environment_variable" "example" { %[2]s - key = "foo" + key = "test_acc_foo_%[1]s" + value = "bar" + target = ["production"] + project_ids = [ + vercel_project.example.id + ] +} + +resource "vercel_shared_environment_variable" "sensitive_example" { + %[2]s + key = "test_acc_foo_sensitive_%[1]s" value = "bar" + sensitive = true target = ["production"] project_ids = [ vercel_project.example.id @@ -138,7 +134,7 @@ resource "vercel_project" "example2" { resource "vercel_shared_environment_variable" "example" { %[2]s - key = "foo" + key = "test_acc_foo_%[1]s" value = "updated-bar" target = ["preview", "development"] project_ids = [ @@ -146,6 +142,18 @@ resource "vercel_shared_environment_variable" "example" { vercel_project.example2.id ] } + +resource "vercel_shared_environment_variable" "sensitive_example" { + %[2]s + key = "test_acc_foo_sensitive_%[1]s" + value = "bar-updated" + sensitive = true + target = ["production"] + project_ids = [ + vercel_project.example.id, + vercel_project.example2.id + ] +} `, projectName, teamIDConfig()) } diff --git a/vercel/validator_serverless_function_region.go b/vercel/validator_serverless_function_region.go index a6767d6c..43d42a1b 100644 --- a/vercel/validator_serverless_function_region.go +++ b/vercel/validator_serverless_function_region.go @@ -33,15 +33,6 @@ func (v validatorServerlessFunctionRegion) MarkdownDescription(ctx context.Conte return fmt.Sprintf("The serverless function region provided is not supported on Vercel. Must be one of `%s`.", strings.Join(keys(v.regions), "`, `")) } -func contains(items []string, i string) bool { - for _, j := range items { - if j == i { - return true - } - } - return false -} - func keys(v map[string]struct{}) (out []string) { for k := range v { out = append(out, k)