Skip to content

Commit

Permalink
Add resource for project_environment_variables
Browse files Browse the repository at this point in the history
This resource can manage many environment variables for a project
together.

This allows users to avoid hitting the API rate limits when using the
terraform provider.
  • Loading branch information
dglsparsons committed Nov 5, 2024
1 parent 551f0f4 commit b1370a3
Show file tree
Hide file tree
Showing 12 changed files with 560 additions and 143 deletions.
77 changes: 52 additions & 25 deletions client/environment_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEn
if err2 != nil {
return e, err2
}
id, err3 := c.findConflictingEnvID(ctx, request.TeamID, request.ProjectID, conflictingEnv)
if err3 != nil {
return e, fmt.Errorf("%w %s", err, err3)

envs, err3 := c.ListEnvironmentVariables(ctx, request.TeamID, request.ProjectID)
if err != nil {
return e, fmt.Errorf("%s: unable to list environment variables to detect conflict: %s", err, err3)
}

id, found := findConflictingEnvID(request.TeamID, request.ProjectID, conflictingEnv, envs)
if found {
return e, fmt.Errorf("%w the conflicting environment variable ID is %s", err, id)
}
return e, fmt.Errorf("%w the conflicting environment variable ID is %s", err, id)
}
if err != nil {
return e, err
Expand Down Expand Up @@ -89,22 +94,17 @@ func overlaps(s []string, e []string) bool {
return false
}

func (c *Client) findConflictingEnvID(ctx context.Context, teamID, projectID string, envConflict EnvConflictError) (string, error) {
envs, err := c.ListEnvironmentVariables(ctx, teamID, projectID)
if err != nil {
return "", fmt.Errorf("unable to list environment variables to detect conflict: %w", err)
}

func findConflictingEnvID(teamID, projectID string, envConflict EnvConflictError, envs []EnvironmentVariable) (string, bool) {
for _, env := range envs {
if env.Key == envConflict.Key && overlaps(env.Target, envConflict.Target) && env.GitBranch == envConflict.GitBranch {
id := fmt.Sprintf("%s/%s", projectID, env.ID)
if teamID != "" {
id = fmt.Sprintf("%s/%s", teamID, id)
}
return id, nil
return id, true
}
}
return "", fmt.Errorf("conflicting environment variable not found")
return "", false
}

type CreateEnvironmentVariablesRequest struct {
Expand All @@ -113,7 +113,20 @@ type CreateEnvironmentVariablesRequest struct {
TeamID string
}

func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateEnvironmentVariablesRequest) error {
type CreateEnvironmentVariablesResponse struct {
Created []EnvironmentVariable `json:"created"`
Failed []struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Key string `json:"envVarKey"`
GitBranch *string `json:"gitBranch"`
Target []string `json:"target"`
} `json:"error"`
} `json:"failed"`
}

func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateEnvironmentVariablesRequest) ([]EnvironmentVariable, error) {
url := fmt.Sprintf("%s/v10/projects/%s/env", c.baseURL, request.ProjectID)
if c.teamID(request.TeamID) != "" {
url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID))
Expand All @@ -123,27 +136,41 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE
"url": url,
"payload": payload,
})

var response CreateEnvironmentVariablesResponse
err := c.doRequest(clientRequest{
ctx: ctx,
method: "POST",
url: url,
body: payload,
}, nil)
if conflictingEnv, isConflicting, err2 := conflictingEnvVar(err); isConflicting {
if err2 != nil {
return err2
}, &response)
if err != nil {
return nil, err
}

if len(response.Failed) > 0 {
envs, err := c.ListEnvironmentVariables(ctx, request.TeamID, request.ProjectID)
if err != nil {
return response.Created, fmt.Errorf("failed to create environment variables. error detecting conflicting environment variables: %w", err)
}
id, err3 := c.findConflictingEnvID(ctx, request.TeamID, request.ProjectID, conflictingEnv)
if err3 != nil {
return fmt.Errorf("%w %s", err, err3)
for _, failed := range response.Failed {
if failed.Error.Code == "ENV_CONFLICT" {
id, found := findConflictingEnvID(request.TeamID, request.ProjectID, EnvConflictError{
Key: failed.Error.Key,
Target: failed.Error.Target,
GitBranch: failed.Error.GitBranch,
}, envs)
if found {
err = fmt.Errorf("%w, conflicting environment variable ID is %s", err, id)
}
} else {
err = fmt.Errorf("failed to create environment variables, %s %s %s", failed.Error.Message, failed.Error.Key, failed.Error.Target)
}
}
return fmt.Errorf("%w the conflicting environment variable ID is %s", err, id)
}
if err != nil {
return err
return response.Created, err
}

return err
return response.Created, err
}

// UpdateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to
Expand Down
1 change: 1 addition & 0 deletions client/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type EnvironmentVariable struct {
ID string `json:"id,omitempty"`
TeamID string `json:"-"`
Comment string `json:"comment"`
Decrypted bool `json:"decrypted"`
}

type DeploymentExpiration struct {
Expand Down
1 change: 1 addition & 0 deletions client/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func (c *Client) _doRequest(req *http.Request, v interface{}, errorOnNoContent b

defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)

if err != nil {
return fmt.Errorf("error reading response body: %w", err)
}
Expand Down
8 changes: 4 additions & 4 deletions docs/resources/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ description: |-
Provides a Project resource.
A Project groups deployments and custom domains. To deploy on Vercel, you need to create a Project.
For more detailed information, please see the Vercel documentation https://vercel.com/docs/concepts/projects/overview.
~> Terraform currently provides both a standalone Project Environment Variable resource (a single Environment Variable), and a Project resource with Environment Variables defined in-line via the environment field.
At this time you cannot use a Vercel Project resource with in-line environment in conjunction with any vercel_project_environment_variable resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
~> Terraform currently provides a standalone Project Environment Variable resource (a single Environment Variable), a Project Environment Variables resource (multiple Environment Variables), and this Project resource with Environment Variables defined in-line via the environment field.
At this time you cannot use a Vercel Project resource with in-line environment in conjunction with any vercel_project_environment_variables or vercel_project_environment_variable resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
---

# vercel_project (Resource)
Expand All @@ -18,8 +18,8 @@ A Project groups deployments and custom domains. To deploy on Vercel, you need t

For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/concepts/projects/overview).

~> Terraform currently provides both a standalone Project Environment Variable resource (a single Environment Variable), and a Project resource with Environment Variables defined in-line via the `environment` field.
At this time you cannot use a Vercel Project resource with in-line `environment` in conjunction with any `vercel_project_environment_variable` resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
~> Terraform currently provides a standalone Project Environment Variable resource (a single Environment Variable), a Project Environment Variables resource (multiple Environment Variables), and this Project resource with Environment Variables defined in-line via the `environment` field.
At this time you cannot use a Vercel Project resource with in-line `environment` in conjunction with any `vercel_project_environment_variables` or `vercel_project_environment_variable` resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.

## Example Usage

Expand Down
8 changes: 4 additions & 4 deletions docs/resources/project_environment_variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ description: |-
Provides a Project Environment Variable resource.
A Project Environment Variable resource defines an Environment Variable on a Vercel Project.
For more detailed information, please see the Vercel documentation https://vercel.com/docs/concepts/projects/environment-variables.
~> Terraform currently provides both a standalone Project Environment Variable resource (a single Environment Variable), and a Project resource with Environment Variables defined in-line via the environment field.
At this time you cannot use a Vercel Project resource with in-line environment in conjunction with any vercel_project_environment_variable resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
~> Terraform currently provides this Project Environment Variable resource (a single Environment Variable), a Project Environment Variables resource (multiple Environment Variables), and a Project resource with Environment Variables defined in-line via the environment field.
At this time you cannot use a Vercel Project resource with in-line environment in conjunction with any vercel_project_environment_variables or vercel_project_environment_variable resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
---

# vercel_project_environment_variable (Resource)
Expand All @@ -18,8 +18,8 @@ A Project Environment Variable resource defines an Environment Variable on a Ver

For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/concepts/projects/environment-variables).

~> Terraform currently provides both a standalone Project Environment Variable resource (a single Environment Variable), and a Project resource with Environment Variables defined in-line via the `environment` field.
At this time you cannot use a Vercel Project resource with in-line `environment` in conjunction with any `vercel_project_environment_variable` resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
~> Terraform currently provides this Project Environment Variable resource (a single Environment Variable), a Project Environment Variables resource (multiple Environment Variables), and a Project resource with Environment Variables defined in-line via the `environment` field.
At this time you cannot use a Vercel Project resource with in-line `environment` in conjunction with any `vercel_project_environment_variables` or `vercel_project_environment_variable` resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.

## Example Usage

Expand Down
89 changes: 89 additions & 0 deletions docs/resources/project_environment_variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "vercel_project_environment_variables Resource - terraform-provider-vercel"
subcategory: ""
description: |-
Provides a resource for managing a number of Project Environment Variables.
This resource defines multiple Environment Variables on a Vercel Project.
For more detailed information, please see the Vercel documentation https://vercel.com/docs/concepts/projects/environment-variables.
~> Terraform currently provides this Project Environment Variables resource (multiple Environment Variables), a single Project Environment Variable Resource, and a Project resource with Environment Variables defined in-line via the environment field.
At this time you cannot use a Vercel Project resource with in-line environment in conjunction with any vercel_project_environment_variables or vercel_project_environment_variable resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.
---

# vercel_project_environment_variables (Resource)

Provides a resource for managing a number of Project Environment Variables.

This resource defines multiple Environment Variables on a Vercel Project.

For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/concepts/projects/environment-variables).

~> Terraform currently provides this Project Environment Variables resource (multiple Environment Variables), a single Project Environment Variable Resource, and a Project resource with Environment Variables defined in-line via the `environment` field.
At this time you cannot use a Vercel Project resource with in-line `environment` in conjunction with any `vercel_project_environment_variables` or `vercel_project_environment_variable` resources. Doing so will cause a conflict of settings and will overwrite Environment Variables.

## Example Usage

```terraform
resource "vercel_project" "example" {
name = "example-project"
git_repository = {
type = "github"
repo = "vercel/some-repo"
}
}
resource "vercel_project_environment_variables" "example" {
project_id = vercel_project.test.id
variables = [
{
key = "SOME_VARIABLE"
value = "some_value"
target = ["production", "preview"]
},
{
key = "ANOTHER_VARIABLE"
value = "another_value"
git_branch = "staging"
target = ["preview"]
},
{
key = "SENSITIVE_VARIABLE"
value = "sensitive_value"
target = ["production"]
sensitive = true
}
]
}
```

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

### Required

- `project_id` (String) The ID of the Vercel project.
- `variables` (Attributes Set) A set of Environment Variables that should be configured for the project. (see [below for nested schema](#nestedatt--variables))

### Optional

- `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.

<a id="nestedatt--variables"></a>
### Nested Schema for `variables`

Required:

- `key` (String) The name of the Environment Variable.
- `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.

Optional:

- `comment` (String) A comment explaining what the environment variable is for.
- `git_branch` (String) The git branch of the Environment Variable.
- `sensitive` (Boolean) Whether the Environment Variable is sensitive or not.

Read-Only:

- `id` (String) The ID of the Environment Variable.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
resource "vercel_project" "example" {
name = "example-project"

git_repository = {
type = "github"
repo = "vercel/some-repo"
}
}

resource "vercel_project_environment_variables" "example" {
project_id = vercel_project.test.id
variables = [
{
key = "SOME_VARIABLE"
value = "some_value"
target = ["production", "preview"]
},
{
key = "ANOTHER_VARIABLE"
value = "another_value"
git_branch = "staging"
target = ["preview"]
},
{
key = "SENSITIVE_VARIABLE"
value = "sensitive_value"
target = ["production"]
sensitive = true
}
]
}
1 change: 1 addition & 0 deletions vercel/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource
newProjectDeploymentRetentionResource,
newProjectDomainResource,
newProjectEnvironmentVariableResource,
newProjectEnvironmentVariablesResource,
newProjectResource,
newSharedEnvironmentVariableResource,
newTeamConfigResource,
Expand Down
2 changes: 1 addition & 1 deletion vercel/resource_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,7 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest
}

if items != nil {
err = r.client.CreateEnvironmentVariables(
_, err = r.client.CreateEnvironmentVariables(
ctx,
client.CreateEnvironmentVariablesRequest{
ProjectID: plan.ID.ValueString(),
Expand Down
18 changes: 9 additions & 9 deletions vercel/resource_project_environment_variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func testAccProjectEnvironmentVariableExists(n, teamID string) resource.TestChec
}
}

func testAccProjectEnvironmentVariablesDoNotExist(n, teamID string) resource.TestCheckFunc {
func testAccProjectEnvironmentVariableDoNotExist(n, teamID string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
Expand All @@ -50,7 +50,7 @@ func testAccProjectEnvironmentVariablesDoNotExist(n, teamID string) resource.Tes
}
}

func TestAcc_ProjectEnvironmentVariables(t *testing.T) {
func TestAcc_ProjectEnvironmentVariable(t *testing.T) {
nameSuffix := acctest.RandString(16)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand All @@ -60,7 +60,7 @@ func TestAcc_ProjectEnvironmentVariables(t *testing.T) {
),
Steps: []resource.TestStep{
{
Config: testAccProjectEnvironmentVariablesConfig(nameSuffix),
Config: testAccProjectEnvironmentVariableConfig(nameSuffix),
Check: resource.ComposeAggregateTestCheckFunc(
testAccProjectEnvironmentVariableExists("vercel_project_environment_variable.example", testTeam()),
resource.TestCheckResourceAttr("vercel_project_environment_variable.example", "key", "foo"),
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestAcc_ProjectEnvironmentVariables(t *testing.T) {
),
},
{
Config: testAccProjectEnvironmentVariablesConfigUpdated(nameSuffix),
Config: testAccProjectEnvironmentVariableConfigUpdated(nameSuffix),
Check: resource.ComposeAggregateTestCheckFunc(
testAccProjectEnvironmentVariableExists("vercel_project_environment_variable.example", testTeam()),
resource.TestCheckResourceAttr("vercel_project_environment_variable.example", "key", "foo"),
Expand Down Expand Up @@ -123,9 +123,9 @@ func TestAcc_ProjectEnvironmentVariables(t *testing.T) {
ImportStateIdFunc: getProjectEnvironmentVariableImportID("vercel_project_environment_variable.example_git_branch"),
},
{
Config: testAccProjectEnvironmentVariablesConfigDeleted(nameSuffix),
Config: testAccProjectEnvironmentVariableConfigDeleted(nameSuffix),
Check: resource.ComposeAggregateTestCheckFunc(
testAccProjectEnvironmentVariablesDoNotExist("vercel_project.example", testTeam()),
testAccProjectEnvironmentVariableDoNotExist("vercel_project.example", testTeam()),
),
},
},
Expand All @@ -150,7 +150,7 @@ func getProjectEnvironmentVariableImportID(n string) resource.ImportStateIdFunc
}
}

func testAccProjectEnvironmentVariablesConfig(projectName string) string {
func testAccProjectEnvironmentVariableConfig(projectName string) string {
return fmt.Sprintf(`
resource "vercel_project" "example" {
name = "test-acc-example-project-%[1]s"
Expand Down Expand Up @@ -200,7 +200,7 @@ resource "vercel_project_environment_variable" "example_not_sensitive" {
`, projectName, testGithubRepo(), teamIDConfig())
}

func testAccProjectEnvironmentVariablesConfigUpdated(projectName string) string {
func testAccProjectEnvironmentVariableConfigUpdated(projectName string) string {
return fmt.Sprintf(`
resource "vercel_project" "example" {
name = "test-acc-example-project-%[1]s"
Expand Down Expand Up @@ -241,7 +241,7 @@ resource "vercel_project_environment_variable" "example_sensitive" {
`, projectName, testGithubRepo(), teamIDConfig())
}

func testAccProjectEnvironmentVariablesConfigDeleted(projectName string) string {
func testAccProjectEnvironmentVariableConfigDeleted(projectName string) string {
return fmt.Sprintf(`
resource "vercel_project" "example" {
name = "test-acc-example-project-%[1]s"
Expand Down
Loading

0 comments on commit b1370a3

Please sign in to comment.