From 04def107fc90bfc7c4913cbb254ec2386e65d03d Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Tue, 17 Dec 2024 17:08:53 +0000 Subject: [PATCH 1/9] Support Custom Environments --- client/custom_environment.go | 137 +++++++ client/request.go | 2 +- docs/data-sources/custom_environment.md | 53 +++ docs/resources/custom_environment.md | 82 ++++ .../vercel_custom_environment/data-source.tf | 8 + .../vercel_custom_environment/import.sh | 11 + .../vercel_custom_environment/resource.tf | 13 + vercel/data_source_custom_environment.go | 134 +++++++ vercel/data_source_custom_environment_test.go | 57 +++ vercel/provider.go | 2 + vercel/resource_custom_environment.go | 376 ++++++++++++++++++ vercel/resource_custom_environment_test.go | 148 +++++++ 12 files changed, 1022 insertions(+), 1 deletion(-) create mode 100644 client/custom_environment.go create mode 100644 docs/data-sources/custom_environment.md create mode 100644 docs/resources/custom_environment.md create mode 100644 examples/data-sources/vercel_custom_environment/data-source.tf create mode 100644 examples/resources/vercel_custom_environment/import.sh create mode 100644 examples/resources/vercel_custom_environment/resource.tf create mode 100644 vercel/data_source_custom_environment.go create mode 100644 vercel/data_source_custom_environment_test.go create mode 100644 vercel/resource_custom_environment.go create mode 100644 vercel/resource_custom_environment_test.go diff --git a/client/custom_environment.go b/client/custom_environment.go new file mode 100644 index 00000000..99a0c5cc --- /dev/null +++ b/client/custom_environment.go @@ -0,0 +1,137 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type BranchMatcher struct { + Pattern string `json:"pattern"` + Type string `json:"type"` +} + +type CreateCustomEnvironmentRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + Slug string `json:"slug"` + Description string `json:"description"` + BranchMatcher *BranchMatcher `json:"branchMatcher,omitempty"` +} + +type CustomEnvironmentResponse struct { + ID string `json:"id"` + Description string `json:"description"` + Slug string `json:"slug"` + BranchMatcher *BranchMatcher `json:"branchMatcher"` + TeamID string `json:"-"` + ProjectID string `json:"-"` +} + +func (c *Client) CreateCustomEnvironment(ctx context.Context, request CreateCustomEnvironmentRequest) (res CustomEnvironmentResponse, err error) { + url := fmt.Sprintf("%s/v1/projects/%s/custom-environments", c.baseURL, request.ProjectID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request)) + tflog.Info(ctx, "creating custom environment", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &res) + if err != nil { + return res, err + } + res.TeamID = c.teamID(request.TeamID) + res.ProjectID = request.ProjectID + return res, nil +} + +type GetCustomEnvironmentRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + Slug string `json:"-"` +} + +func (c *Client) GetCustomEnvironment(ctx context.Context, request GetCustomEnvironmentRequest) (res CustomEnvironmentResponse, err error) { + url := fmt.Sprintf("%s/v1/projects/%s/custom-environments/%s", c.baseURL, request.ProjectID, request.Slug) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + tflog.Info(ctx, "getting custom environment", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &res) + if err != nil { + return res, err + } + res.TeamID = c.teamID(request.TeamID) + res.ProjectID = request.ProjectID + return res, nil + +} + +type UpdateCustomEnvironmentRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + Slug string `json:"slug"` + Description string `json:"description"` + BranchMatcher *BranchMatcher `json:"branchMatcher"` +} + +func (c *Client) UpdateCustomEnvironment(ctx context.Context, request UpdateCustomEnvironmentRequest) (res CustomEnvironmentResponse, err error) { + url := fmt.Sprintf("%s/v1/projects/%s/custom-environments/%s", c.baseURL, request.ProjectID, request.Slug) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request)) + tflog.Info(ctx, "updating custom environment", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &res) + if err != nil { + return res, err + } + res.TeamID = c.teamID(request.TeamID) + res.ProjectID = request.ProjectID + return res, nil +} + +type DeleteCustomEnvironmentRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + Slug string `json:"-"` +} + +func (c *Client) DeleteCustomEnvironment(ctx context.Context, request DeleteCustomEnvironmentRequest) error { + url := fmt.Sprintf("%s/v1/projects/%s/custom-environments/%s", c.baseURL, request.ProjectID, request.Slug) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + tflog.Info(ctx, "deleting custom environment", map[string]interface{}{ + "url": url, + }) + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "{ \"deleteUnassignedEnvironmentVariables\": true }", + }, nil) + return err +} diff --git a/client/request.go b/client/request.go index 50a60241..7d54e00a 100644 --- a/client/request.go +++ b/client/request.go @@ -127,7 +127,7 @@ func (c *Client) _doRequest(req *http.Request, v interface{}, errorOnNoContent b Error: &errorResponse, }) if err != nil { - return fmt.Errorf("error unmarshaling response for status code %d: %w", resp.StatusCode, err) + return fmt.Errorf("error unmarshaling response for status code %d: %w: %s", resp.StatusCode, err, string(responseBody)) } errorResponse.StatusCode = resp.StatusCode errorResponse.RawMessage = responseBody diff --git a/docs/data-sources/custom_environment.md b/docs/data-sources/custom_environment.md new file mode 100644 index 00000000..26a090c7 --- /dev/null +++ b/docs/data-sources/custom_environment.md @@ -0,0 +1,53 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_custom_environment Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides information about an existing CustomEnvironment resource. + An CustomEnvironment allows a vercel_deployment to be accessed through a different URL. +--- + +# vercel_custom_environment (Data Source) + +Provides information about an existing CustomEnvironment resource. + +An CustomEnvironment allows a `vercel_deployment` to be accessed through a different URL. + +## Example Usage + +```terraform +data "vercel_project" "example" { + name = "example-project-with-custom-env" +} + +data "vercel_custom_environment" "example" { + project_id = data.vercel_project.example.id + name = "example-custom-env" +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the environment. +- `project_id` (String) The ID of the existing Vercel Project. + +### Optional + +- `team_id` (String) The team ID to add the project to. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `branch_tracking` (Attributes) The branch tracking configuration for the environment. When enabled, each qualifying merge will generate a deployment. (see [below for nested schema](#nestedatt--branch_tracking)) +- `description` (String) A description of what the environment is. +- `id` (String) The ID of the environment. + + +### Nested Schema for `branch_tracking` + +Read-Only: + +- `pattern` (String) The pattern of the branch name to track. +- `type` (String) How a branch name should be matched against the pattern. Must be one of 'startsWith', 'endsWith' or 'equals'. diff --git a/docs/resources/custom_environment.md b/docs/resources/custom_environment.md new file mode 100644 index 00000000..fac6ae10 --- /dev/null +++ b/docs/resources/custom_environment.md @@ -0,0 +1,82 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_custom_environment Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Environments help manage the deployment lifecycle on the Vercel platform. + By default, all teams use three environments when developing their project: Production, Preview, and Development. However, teams can also create custom environments to suit their needs. To learn more about the limits for each plan, see limits. + Custom environments allow you to configure customized, pre-production environments for your project, such as staging or QA, with branch rules that will automatically deploy your branch when the branch name matches the rule. With custom environments you can also attach a domain to your environment, set environment variables, or import environment variables from another environment. + Custom environments are designed as pre-production environments intended for long-running use. This contrasts with regular preview environments, which are designed for creating ephemeral, short-lived deployments. +--- + +# vercel_custom_environment (Resource) + +Environments help manage the deployment lifecycle on the Vercel platform. + +By default, all teams use three environments when developing their project: Production, Preview, and Development. However, teams can also create custom environments to suit their needs. To learn more about the limits for each plan, see limits. + +Custom environments allow you to configure customized, pre-production environments for your project, such as staging or QA, with branch rules that will automatically deploy your branch when the branch name matches the rule. With custom environments you can also attach a domain to your environment, set environment variables, or import environment variables from another environment. + +Custom environments are designed as pre-production environments intended for long-running use. This contrasts with regular preview environments, which are designed for creating ephemeral, short-lived deployments. + +## Example Usage + +```terraform +resource "vercel_project" "example" { + name = "example-project-with-custom-env" +} + +resource "vercel_custom_environment" "example" { + project_id = vercel_project.example.id + name = "example-custom-env" + description = "A description of the custom environment" + branch_tracking = { + pattern = "staging-" + type = "startsWith" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the environment. +- `project_id` (String) The ID of the existing Vercel Project. + +### Optional + +- `branch_tracking` (Attributes) The branch tracking configuration for the environment. When enabled, each qualifying merge will generate a deployment. (see [below for nested schema](#nestedatt--branch_tracking)) +- `description` (String) A description of what the environment is. +- `team_id` (String) The team ID to add the project to. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `id` (String) The ID of the environment. + + +### Nested Schema for `branch_tracking` + +Required: + +- `pattern` (String) The pattern of the branch name to track. +- `type` (String) How a branch name should be matched against the pattern. Must be one of 'startsWith', 'endsWith' or 'equals'. + +## Import + +Import is supported using the following syntax: + +```shell +# If importing into a personal account, or with a team configured on +# the provider, simply use the project_id and custom environment name. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_custom_environment.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/example-custom-env + +# Alternatively, you can import via the team_id, project_id and environment variable id. +# - 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. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. +terraform import vercel_custom_environment.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/example-custom-env +``` diff --git a/examples/data-sources/vercel_custom_environment/data-source.tf b/examples/data-sources/vercel_custom_environment/data-source.tf new file mode 100644 index 00000000..1a8d49de --- /dev/null +++ b/examples/data-sources/vercel_custom_environment/data-source.tf @@ -0,0 +1,8 @@ +data "vercel_project" "example" { + name = "example-project-with-custom-env" +} + +data "vercel_custom_environment" "example" { + project_id = data.vercel_project.example.id + name = "example-custom-env" +} diff --git a/examples/resources/vercel_custom_environment/import.sh b/examples/resources/vercel_custom_environment/import.sh new file mode 100644 index 00000000..b6f0718e --- /dev/null +++ b/examples/resources/vercel_custom_environment/import.sh @@ -0,0 +1,11 @@ +# If importing into a personal account, or with a team configured on +# the provider, simply use the project_id and custom environment name. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_custom_environment.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/example-custom-env + +# Alternatively, you can import via the team_id, project_id and environment variable id. +# - 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. +# +# Note also, that the value field for sensitive environment variables will be imported as `null`. +terraform import vercel_custom_environment.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/example-custom-env diff --git a/examples/resources/vercel_custom_environment/resource.tf b/examples/resources/vercel_custom_environment/resource.tf new file mode 100644 index 00000000..b7d55674 --- /dev/null +++ b/examples/resources/vercel_custom_environment/resource.tf @@ -0,0 +1,13 @@ +resource "vercel_project" "example" { + name = "example-project-with-custom-env" +} + +resource "vercel_custom_environment" "example" { + project_id = vercel_project.example.id + name = "example-custom-env" + description = "A description of the custom environment" + branch_tracking = { + pattern = "staging-" + type = "startsWith" + } +} diff --git a/vercel/data_source_custom_environment.go b/vercel/data_source_custom_environment.go new file mode 100644 index 00000000..038a933e --- /dev/null +++ b/vercel/data_source_custom_environment.go @@ -0,0 +1,134 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &customEnvironmentDataSource{} +) + +func newCustomEnvironmentDataSource() datasource.DataSource { + return &customEnvironmentDataSource{} +} + +type customEnvironmentDataSource struct { + client *client.Client +} + +func (d *customEnvironmentDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_custom_environment" +} + +func (d *customEnvironmentDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +// Schema returns the schema information for an customEnvironment data source +func (r *customEnvironmentDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides information about an existing CustomEnvironment resource. + +An CustomEnvironment allows a ` + "`vercel_deployment` to be accessed through a different URL.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of the environment.", + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The team ID to add the project to. Required when configuring a team resource if a default team has not been set in the provider.", + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the existing Vercel Project.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the environment.", + Required: true, + }, + "description": schema.StringAttribute{ + Description: "A description of what the environment is.", + Computed: true, + }, + "branch_tracking": schema.SingleNestedAttribute{ + Description: "The branch tracking configuration for the environment. When enabled, each qualifying merge will generate a deployment.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "pattern": schema.StringAttribute{ + Description: "The pattern of the branch name to track.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "How a branch name should be matched against the pattern. Must be one of 'startsWith', 'endsWith' or 'equals'.", + Computed: true, + }, + }, + }, + }, + } +} + +// Read will read the customEnvironment information by requesting it from the Vercel API, and will update terraform +// with this information. +// It is called by the provider whenever data source values should be read to update state. +func (d *customEnvironmentDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config CustomEnvironment + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + res, err := d.client.GetCustomEnvironment(ctx, client.GetCustomEnvironmentRequest{ + TeamID: config.TeamID.ValueString(), + ProjectID: config.ProjectID.ValueString(), + Slug: config.Name.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error reading custom environment", + fmt.Sprintf("Could not read custom environment %s %s %s, unexpected error: %s", + config.TeamID.ValueString(), + config.ProjectID.ValueString(), + config.Name.ValueString(), + err, + ), + ) + return + } + tflog.Trace(ctx, "read custom environment", map[string]interface{}{ + "team_id": config.TeamID.ValueString(), + "project_id": config.ProjectID.ValueString(), + "custom_environment_id": res.ID, + }) + + diags = resp.State.Set(ctx, convertResponseToModel(res)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_custom_environment_test.go b/vercel/data_source_custom_environment_test.go new file mode 100644 index 00000000..629de6fd --- /dev/null +++ b/vercel/data_source_custom_environment_test.go @@ -0,0 +1,57 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_CustomEnvironmentDataSource(t *testing.T) { + projectSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccProjectDestroy("vercel_project.test", testTeam()), + Steps: []resource.TestStep{ + { + Config: testAccCustomEnvironmentDataSource(projectSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.vercel_custom_environment.test", "id"), + resource.TestCheckResourceAttrSet("data.vercel_custom_environment.test", "project_id"), + resource.TestCheckResourceAttrSet("data.vercel_custom_environment.test", "name"), + resource.TestCheckResourceAttr("data.vercel_custom_environment.test", "branch_tracking.type", "startsWith"), + resource.TestCheckResourceAttr("data.vercel_custom_environment.test", "branch_tracking.pattern", "staging-"), + resource.TestCheckResourceAttr("data.vercel_custom_environment.test", "description", "oh cool"), + ), + }, + }, + }) +} + +func testAccCustomEnvironmentDataSource(projectSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + name = "test-acc-custom-env-data-source-%[1]s" + %[2]s +} + +resource "vercel_custom_environment" "test" { + project_id = vercel_project.test.id + %[2]s + name = "test-acc-custom-env-%[1]s" + description = "oh cool" + branch_tracking = { + pattern = "staging-" + type = "startsWith" + } +} + +data "vercel_custom_environment" "test" { + project_id = vercel_project.test.id + %[2]s + name = "test-acc-custom-env-%[1]s" +} +`, projectSuffix, teamIDConfig()) +} diff --git a/vercel/provider.go b/vercel/provider.go index a0a54e17..8dcb97ec 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -54,6 +54,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newAccessGroupResource, newAliasResource, newAttackChallengeModeResource, + newCustomEnvironmentResource, newDNSRecordResource, newDeploymentResource, newEdgeConfigItemResource, @@ -81,6 +82,7 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newAccessGroupProjectDataSource, newAliasDataSource, newAttackChallengeModeDataSource, + newCustomEnvironmentDataSource, newDeploymentDataSource, newEdgeConfigDataSource, newEdgeConfigItemDataSource, diff --git a/vercel/resource_custom_environment.go b/vercel/resource_custom_environment.go new file mode 100644 index 00000000..97398f9d --- /dev/null +++ b/vercel/resource_custom_environment.go @@ -0,0 +1,376 @@ +package vercel + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +var ( + _ resource.Resource = &customEnvironmentResource{} + _ resource.ResourceWithConfigure = &customEnvironmentResource{} + _ resource.ResourceWithImportState = &customEnvironmentResource{} +) + +func newCustomEnvironmentResource() resource.Resource { + return &customEnvironmentResource{} +} + +type customEnvironmentResource struct { + client *client.Client +} + +func (r *customEnvironmentResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_custom_environment" +} + +func (r *customEnvironmentResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *customEnvironmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Environments help manage the deployment lifecycle on the Vercel platform. + +By default, all teams use three environments when developing their project: Production, Preview, and Development. However, teams can also create custom environments to suit their needs. To learn more about the limits for each plan, see limits. + +Custom environments allow you to configure customized, pre-production environments for your project, such as staging or QA, with branch rules that will automatically deploy your branch when the branch name matches the rule. With custom environments you can also attach a domain to your environment, set environment variables, or import environment variables from another environment. + +Custom environments are designed as pre-production environments intended for long-running use. This contrasts with regular preview environments, which are designed for creating ephemeral, short-lived deployments. +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of the environment.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + Description: "The team ID to add the project to. Required when configuring a team resource if a default team has not been set in the provider.", + }, + "project_id": schema.StringAttribute{ + Description: "The ID of the existing Vercel Project.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "name": schema.StringAttribute{ + Description: "The name of the environment.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 32), + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-z0-9\-]{0,32}$`), + "The name of a Custom Environment can only contain up to 32 alphanumeric lowercase characters and hyphens", + ), + }, + }, + "description": schema.StringAttribute{ + Description: "A description of what the environment is.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "branch_tracking": schema.SingleNestedAttribute{ + Description: "The branch tracking configuration for the environment. When enabled, each qualifying merge will generate a deployment.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + Attributes: map[string]schema.Attribute{ + "pattern": schema.StringAttribute{ + Description: "The pattern of the branch name to track.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 100), + }, + }, + "type": schema.StringAttribute{ + Description: "How a branch name should be matched against the pattern. Must be one of 'startsWith', 'endsWith' or 'equals'.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("startsWith", "endsWith", "equals"), + }, + }, + }, + }, + }, + } +} + +type BranchTracking struct { + Pattern types.String `tfsdk:"pattern"` + Type types.String `tfsdk:"type"` +} + +type CustomEnvironment struct { + ID types.String `tfsdk:"id"` + TeamID types.String `tfsdk:"team_id"` + ProjectID types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + BranchTracking types.Object `tfsdk:"branch_tracking"` +} + +func (c CustomEnvironment) toCreateRequest(ctx context.Context) (client.CreateCustomEnvironmentRequest, diag.Diagnostics) { + var bm *client.BranchMatcher + if !c.BranchTracking.IsNull() && !c.BranchTracking.IsUnknown() { + bt, diags := c.branchTracking(ctx) + if diags.HasError() { + return client.CreateCustomEnvironmentRequest{}, diags + } + bm = &client.BranchMatcher{ + Pattern: bt.Pattern.ValueString(), + Type: bt.Type.ValueString(), + } + } + return client.CreateCustomEnvironmentRequest{ + TeamID: c.TeamID.ValueString(), + ProjectID: c.ProjectID.ValueString(), + Slug: c.Name.ValueString(), + Description: c.Description.ValueString(), + BranchMatcher: bm, + }, nil +} + +func (c CustomEnvironment) toUpdateRequest(ctx context.Context) (client.UpdateCustomEnvironmentRequest, diag.Diagnostics) { + var bm *client.BranchMatcher + if !c.BranchTracking.IsNull() && !c.BranchTracking.IsUnknown() { + bt, diags := c.branchTracking(ctx) + if diags.HasError() { + return client.UpdateCustomEnvironmentRequest{}, diags + } + bm = &client.BranchMatcher{ + Pattern: bt.Pattern.ValueString(), + Type: bt.Type.ValueString(), + } + } + return client.UpdateCustomEnvironmentRequest{ + TeamID: c.TeamID.ValueString(), + ProjectID: c.ProjectID.ValueString(), + Slug: c.Name.ValueString(), + Description: c.Description.ValueString(), + BranchMatcher: bm, + }, nil +} + +func (c CustomEnvironment) branchTracking(ctx context.Context) (BranchTracking, diag.Diagnostics) { + var bt BranchTracking + diags := c.BranchTracking.As(ctx, &bt, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + }) + return bt, diags +} + +var branchTrackingAttrType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "pattern": types.StringType, + "type": types.StringType, + }, +} + +func convertResponseToModel(res client.CustomEnvironmentResponse) CustomEnvironment { + bt := types.ObjectNull(branchTrackingAttrType.AttrTypes) + if res.BranchMatcher != nil { + bt = types.ObjectValueMust( + branchTrackingAttrType.AttrTypes, map[string]attr.Value{ + "pattern": types.StringValue(res.BranchMatcher.Pattern), + "type": types.StringValue(res.BranchMatcher.Type), + }) + } + return CustomEnvironment{ + ID: types.StringValue(res.ID), + TeamID: types.StringValue(res.TeamID), + ProjectID: types.StringValue(res.ProjectID), + Name: types.StringValue(res.Slug), + Description: types.StringValue(res.Description), + BranchTracking: bt, + } +} + +func (r *customEnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan CustomEnvironment + diags := req.Plan.Get(ctx, &plan) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + createRequest, diags := plan.toCreateRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + res, err := r.client.CreateCustomEnvironment(ctx, createRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error creating custom environment", + fmt.Sprintf("Could not create custom environment, unexpected error: %s", err), + ) + return + } + + tflog.Info(ctx, "created custom environment", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "project_id": plan.ProjectID.ValueString(), + "custom_environment_id": res.ID, + "total_res": res, + }) + + diags = resp.State.Set(ctx, convertResponseToModel(res)) + resp.Diagnostics.Append(diags...) +} + +func (r *customEnvironmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state CustomEnvironment + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + res, err := r.client.GetCustomEnvironment(ctx, client.GetCustomEnvironmentRequest{ + TeamID: state.TeamID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + Slug: state.Name.ValueString(), + }) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading custom environment", + fmt.Sprintf("Could not read custom environment, unexpected error: %s", err), + ) + return + } + tflog.Trace(ctx, "read custom environment", map[string]interface{}{ + "team_id": state.TeamID.ValueString(), + "project_id": state.ProjectID.ValueString(), + "custom_environment_id": res.ID, + }) + + diags = resp.State.Set(ctx, convertResponseToModel(res)) + resp.Diagnostics.Append(diags...) +} + +func (r *customEnvironmentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan CustomEnvironment + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + updateRequest, diags := plan.toUpdateRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + res, err := r.client.UpdateCustomEnvironment(ctx, updateRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error updating custom environment", + fmt.Sprintf("Could not update custom environment, unexpected error: %s", err), + ) + return + } + + tflog.Trace(ctx, "created custom environment", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "project_id": plan.ProjectID.ValueString(), + "custom_environment_id": res.ID, + }) + + diags = resp.State.Set(ctx, convertResponseToModel(res)) + resp.Diagnostics.Append(diags...) +} + +func (r *customEnvironmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state CustomEnvironment + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteCustomEnvironment(ctx, client.DeleteCustomEnvironmentRequest{ + ProjectID: state.ProjectID.ValueString(), + TeamID: state.TeamID.ValueString(), + Slug: state.Name.ValueString(), + }) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + } + if err != nil { + resp.Diagnostics.AddError( + "Error removing custom environment", + fmt.Sprintf("Could not remove custom environment: %s", err), + ) + return + } +} + +// ImportState implements resource.ResourceWithImportState. +func (r *customEnvironmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, projectID, name, ok := splitInto2Or3(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Custom Environment", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id/custom_environment_name\" or \"project_id/custom_environment_name\"", req.ID), + ) + } + res, err := r.client.GetCustomEnvironment(ctx, client.GetCustomEnvironmentRequest{ + TeamID: teamID, + ProjectID: projectID, + Slug: name, + }) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading custom environment", + fmt.Sprintf("Could not read custom environment, unexpected error: %s", err), + ) + return + } + tflog.Trace(ctx, "import custom environment", map[string]interface{}{ + "team_id": teamID, + "project_id": projectID, + "custom_environment_id": res.ID, + }) + + diags := resp.State.Set(ctx, convertResponseToModel(res)) + resp.Diagnostics.Append(diags...) +} diff --git a/vercel/resource_custom_environment_test.go b/vercel/resource_custom_environment_test.go new file mode 100644 index 00000000..23fca4cf --- /dev/null +++ b/vercel/resource_custom_environment_test.go @@ -0,0 +1,148 @@ +package vercel_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +func testCheckCustomEnvironmentExists(n 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") + } + + projectID := rs.Primary.Attributes["project_id"] + name := rs.Primary.Attributes["name"] + + _, err := testClient().GetCustomEnvironment(context.TODO(), client.GetCustomEnvironmentRequest{ + TeamID: testTeam(), + ProjectID: projectID, + Slug: name, + }) + if client.NotFound(err) { + return fmt.Errorf("test failed because the custom environment %s %s %s - %s could not be found", testTeam(), projectID, name, rs.Primary.ID) + } + return err + } +} + +func TestAcc_CustomEnvironmentResource(t *testing.T) { + projectSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccProjectDestroy("vercel_project.test", testTeam()), + Steps: []resource.TestStep{ + { + Config: testAccCustomEnvironment(projectSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckCustomEnvironmentExists("vercel_custom_environment.test"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "project_id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "name"), + resource.TestCheckNoResourceAttr("vercel_custom_environment.test", "branch_tracking"), + resource.TestCheckResourceAttr("vercel_custom_environment.test", "description", "without branch tracking"), + + testCheckCustomEnvironmentExists("vercel_custom_environment.test_bt"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "project_id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "name"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.type", "startsWith"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.pattern", "staging-"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "description", "with branch tracking"), + ), + }, + { + Config: testAccCustomEnvironmentUpdated(projectSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckCustomEnvironmentExists("vercel_custom_environment.test"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "project_id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "name"), + resource.TestCheckResourceAttr("vercel_custom_environment.test", "branch_tracking.type", "endsWith"), + resource.TestCheckResourceAttr("vercel_custom_environment.test", "branch_tracking.pattern", "staging-"), + resource.TestCheckResourceAttr("vercel_custom_environment.test", "description", "without branch tracking updated"), + ), + }, + { + ResourceName: "vercel_custom_environment.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getCustomEnvImportID("vercel_shared_environment_variable.example"), + }, + }, + }) +} + +func getCustomEnvImportID(n string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, 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") + } + + return fmt.Sprintf("%s/%s/%s", rs.Primary.Attributes["team_id"], rs.Primary.Attributes["project_id"], rs.Primary.Attributes["name"]), nil + } +} + +func testAccCustomEnvironment(projectSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + name = "test-acc-custom-env-%[1]s" + %[2]s +} + +resource "vercel_custom_environment" "test" { + project_id = vercel_project.test.id + %[2]s + name = "test-acc-%[1]s" + description = "without branch tracking" +} + +resource "vercel_custom_environment" "test_bt" { + project_id = vercel_project.test.id + %[2]s + name = "test-acc-bt-%[1]s" + description = "with branch tracking" + branch_tracking = { + pattern = "staging-" + type = "startsWith" + } +} +`, projectSuffix, teamIDConfig()) +} + +func testAccCustomEnvironmentUpdated(projectSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + name = "test-acc-custom-env-%[1]s" + %[2]s +} + +resource "vercel_custom_environment" "test" { + project_id = vercel_project.test.id + %[2]s + name = "test-acc-%[1]s-updated" + description = "without branch tracking updated" + branch_tracking = { + pattern = "staging-" + type = "endsWith" + } +} +`, projectSuffix, teamIDConfig()) +} From 129b7e8db15c110927a762a77d4baf899d976091 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Tue, 17 Dec 2024 17:17:45 +0000 Subject: [PATCH 2/9] Support environment variable targets for custom environments --- ...data_source_shared_environment_variable.go | 6 +--- vercel/resource_custom_environment_test.go | 34 ++++++++++++++++--- vercel/resource_project.go | 3 +- .../resource_project_environment_variable.go | 3 +- .../resource_project_environment_variables.go | 3 +- .../resource_shared_environment_variable.go | 3 +- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/vercel/data_source_shared_environment_variable.go b/vercel/data_source_shared_environment_variable.go index e2924ce3..2f8aa31e 100644 --- a/vercel/data_source_shared_environment_variable.go +++ b/vercel/data_source_shared_environment_variable.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -107,12 +106,9 @@ For more detailed information, please see the [Vercel documentation](https://ver "target": schema.SetAttribute{ Optional: true, Computed: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", ElementType: types.StringType, Validators: []validator.Set{ - setvalidator.ValueStringsAre( - stringvalidator.OneOf("production", "preview", "development"), - ), setvalidator.SizeAtLeast(1), }, }, diff --git a/vercel/resource_custom_environment_test.go b/vercel/resource_custom_environment_test.go index 23fca4cf..3afde668 100644 --- a/vercel/resource_custom_environment_test.go +++ b/vercel/resource_custom_environment_test.go @@ -50,17 +50,23 @@ func TestAcc_CustomEnvironmentResource(t *testing.T) { testCheckCustomEnvironmentExists("vercel_custom_environment.test"), resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "id"), resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "project_id"), - resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "name"), + resource.TestCheckResourceAttr("vercel_custom_environment.test", "name", "test-acc"), resource.TestCheckNoResourceAttr("vercel_custom_environment.test", "branch_tracking"), resource.TestCheckResourceAttr("vercel_custom_environment.test", "description", "without branch tracking"), testCheckCustomEnvironmentExists("vercel_custom_environment.test_bt"), resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "id"), resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "project_id"), - resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "name"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "name", "test-acc-bt"), resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.type", "startsWith"), resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.pattern", "staging-"), resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "description", "with branch tracking"), + + // check project env var + resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.test", "target.*", "test-acc"), + + // check shared env var + resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.test", "target.*", "test-acc"), ), }, { @@ -110,14 +116,32 @@ resource "vercel_project" "test" { resource "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc-%[1]s" + name = "test-acc" description = "without branch tracking" } +// Ensure project_environment_variable works +resource "vercel_project_environment_variable" "test" { + project_id = vercel_project.test.id + %[2]s + key = "foo" + value = "test-acc-env-var" + target = [vercel_custom_environment.test.name] +} + +// Ensurer shared_environment_variable works +resource "vercel_shared_environment_variable" "test" { + project_ids = [vercel_project.test.id] + %[2]s + key = "bar" + value = "test-acc-shared-env-var" + target = [vercel_custom_environment.test.name] +} + resource "vercel_custom_environment" "test_bt" { project_id = vercel_project.test.id %[2]s - name = "test-acc-bt-%[1]s" + name = "test-acc-bt" description = "with branch tracking" branch_tracking = { pattern = "staging-" @@ -137,7 +161,7 @@ resource "vercel_project" "test" { resource "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc-%[1]s-updated" + name = "test-acc-updated" description = "without branch tracking updated" branch_tracking = { pattern = "staging-" diff --git a/vercel/resource_project.go b/vercel/resource_project.go index f21251b6..2c41b099 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -124,10 +124,9 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", ElementType: types.StringType, Validators: []validator.Set{ - setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, Required: true, diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index aaf7eaaf..be82cf2e 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -71,10 +71,9 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", ElementType: types.StringType, Validators: []validator.Set{ - setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, }, diff --git a/vercel/resource_project_environment_variables.go b/vercel/resource_project_environment_variables.go index 015ee57e..afac9381 100644 --- a/vercel/resource_project_environment_variables.go +++ b/vercel/resource_project_environment_variables.go @@ -101,10 +101,9 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ }, "target": schema.SetAttribute{ Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", ElementType: types.StringType, Validators: []validator.Set{ - setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, }, diff --git a/vercel/resource_shared_environment_variable.go b/vercel/resource_shared_environment_variable.go index f369e43d..65c77cc1 100644 --- a/vercel/resource_shared_environment_variable.go +++ b/vercel/resource_shared_environment_variable.go @@ -113,10 +113,9 @@ For more detailed information, please see the [Vercel documentation](https://ver Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", ElementType: types.StringType, Validators: []validator.Set{ - setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, }, From 362da811fde90b0bd14307ec351a996ffd3b43a1 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Wed, 18 Dec 2024 12:47:56 +0000 Subject: [PATCH 3/9] Support project domains --- client/project_domain.go | 29 ++--- vercel/resource_custom_environment_test.go | 18 +-- vercel/resource_project_domain.go | 64 +++++++---- vercel/resource_project_domain_test.go | 121 ++++++++++++++++++--- 4 files changed, 175 insertions(+), 57 deletions(-) diff --git a/client/project_domain.go b/client/project_domain.go index e6e8df97..d0f22fc2 100644 --- a/client/project_domain.go +++ b/client/project_domain.go @@ -12,10 +12,11 @@ import ( // used to assign a domain name to any production deployments, but can also be used to configure // redirects, or to give specific git branches a domain name. type CreateProjectDomainRequest struct { - Name string `json:"name"` - GitBranch string `json:"gitBranch,omitempty"` - Redirect string `json:"redirect,omitempty"` - RedirectStatusCode int64 `json:"redirectStatusCode,omitempty"` + Name string `json:"name"` + GitBranch string `json:"gitBranch,omitempty"` + CustomEnvironmentID string `json:"customEnvironmentId,omitempty"` + Redirect string `json:"redirect,omitempty"` + RedirectStatusCode int64 `json:"redirectStatusCode,omitempty"` } // CreateProjectDomain creates a project domain within Vercel. @@ -61,12 +62,13 @@ func (c *Client) DeleteProjectDomain(ctx context.Context, projectID, domain, tea // ProjectDomainResponse defines the information that Vercel exposes about a domain that is // associated with a vercel project. type ProjectDomainResponse struct { - Name string `json:"name"` - ProjectID string `json:"projectId"` - TeamID string `json:"-"` - Redirect *string `json:"redirect"` - RedirectStatusCode *int64 `json:"redirectStatusCode"` - GitBranch *string `json:"gitBranch"` + Name string `json:"name"` + ProjectID string `json:"projectId"` + TeamID string `json:"-"` + Redirect *string `json:"redirect"` + RedirectStatusCode *int64 `json:"redirectStatusCode"` + GitBranch *string `json:"gitBranch"` + CustomEnvironmentID *string `json:"customEnvironmentId"` } // GetProjectDomain retrieves information about a project domain from Vercel. @@ -91,9 +93,10 @@ func (c *Client) GetProjectDomain(ctx context.Context, projectID, domain, teamID // UpdateProjectDomainRequest defines the information necessary to update a project domain. type UpdateProjectDomainRequest struct { - GitBranch *string `json:"gitBranch"` - Redirect *string `json:"redirect"` - RedirectStatusCode *int64 `json:"redirectStatusCode"` + GitBranch *string `json:"gitBranch"` + CustomEnvironmentID *string `json:"customEnvironmentId,omitempty"` + Redirect *string `json:"redirect"` + RedirectStatusCode *int64 `json:"redirectStatusCode"` } // UpdateProjectDomain updates an existing project domain within Vercel. diff --git a/vercel/resource_custom_environment_test.go b/vercel/resource_custom_environment_test.go index 3afde668..84efaae1 100644 --- a/vercel/resource_custom_environment_test.go +++ b/vercel/resource_custom_environment_test.go @@ -54,13 +54,15 @@ func TestAcc_CustomEnvironmentResource(t *testing.T) { resource.TestCheckNoResourceAttr("vercel_custom_environment.test", "branch_tracking"), resource.TestCheckResourceAttr("vercel_custom_environment.test", "description", "without branch tracking"), - testCheckCustomEnvironmentExists("vercel_custom_environment.test_bt"), - resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "id"), - resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "project_id"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "name", "test-acc-bt"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.type", "startsWith"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.pattern", "staging-"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "description", "with branch tracking"), + /* + testCheckCustomEnvironmentExists("vercel_custom_environment.test_bt"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "project_id"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "name", "test-acc-bt"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.type", "startsWith"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.pattern", "staging-"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "description", "with branch tracking"), + */ // check project env var resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.test", "target.*", "test-acc"), @@ -138,6 +140,7 @@ resource "vercel_shared_environment_variable" "test" { target = [vercel_custom_environment.test.name] } +/* resource "vercel_custom_environment" "test_bt" { project_id = vercel_project.test.id %[2]s @@ -148,6 +151,7 @@ resource "vercel_custom_environment" "test_bt" { type = "startsWith" } } +*/ `, projectSuffix, teamIDConfig()) } diff --git a/vercel/resource_project_domain.go b/vercel/resource_project_domain.go index fda92518..98019d9f 100644 --- a/vercel/resource_project_domain.go +++ b/vercel/resource_project_domain.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -94,6 +96,22 @@ By default, Project Domains will be automatically applied to any ` + "`productio "git_branch": schema.StringAttribute{ Description: "Git branch to link to the project domain. Deployments from this git branch will be assigned the domain name.", Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith( + path.MatchRoot("custom_environment_id"), + path.MatchRoot("git_branch"), + ), + }, + }, + "custom_environment_id": schema.StringAttribute{ + Description: "The name of the Custom Environment to link to the Project Domain. Deployments from this custom environment will be assigned the domain name.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith( + path.MatchRoot("custom_environment_id"), + path.MatchRoot("git_branch"), + ), + }, }, }, } @@ -101,41 +119,45 @@ By default, Project Domains will be automatically applied to any ` + "`productio // ProjectDomain reflects the state terraform stores internally for a project domain. type ProjectDomain struct { - Domain types.String `tfsdk:"domain"` - GitBranch types.String `tfsdk:"git_branch"` - ID types.String `tfsdk:"id"` - ProjectID types.String `tfsdk:"project_id"` - Redirect types.String `tfsdk:"redirect"` - RedirectStatusCode types.Int64 `tfsdk:"redirect_status_code"` - TeamID types.String `tfsdk:"team_id"` + Domain types.String `tfsdk:"domain"` + GitBranch types.String `tfsdk:"git_branch"` + CustomEnvironmentID types.String `tfsdk:"custom_environment_id"` + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Redirect types.String `tfsdk:"redirect"` + RedirectStatusCode types.Int64 `tfsdk:"redirect_status_code"` + TeamID types.String `tfsdk:"team_id"` } func convertResponseToProjectDomain(response client.ProjectDomainResponse) ProjectDomain { return ProjectDomain{ - Domain: types.StringValue(response.Name), - GitBranch: types.StringPointerValue(response.GitBranch), - ID: types.StringValue(response.Name), - ProjectID: types.StringValue(response.ProjectID), - Redirect: types.StringPointerValue(response.Redirect), - RedirectStatusCode: types.Int64PointerValue(response.RedirectStatusCode), - TeamID: toTeamID(response.TeamID), + Domain: types.StringValue(response.Name), + GitBranch: types.StringPointerValue(response.GitBranch), + CustomEnvironmentID: types.StringPointerValue(response.CustomEnvironmentID), + ID: types.StringValue(response.Name), + ProjectID: types.StringValue(response.ProjectID), + Redirect: types.StringPointerValue(response.Redirect), + RedirectStatusCode: types.Int64PointerValue(response.RedirectStatusCode), + TeamID: toTeamID(response.TeamID), } } func (p *ProjectDomain) toCreateRequest() client.CreateProjectDomainRequest { return client.CreateProjectDomainRequest{ - GitBranch: p.GitBranch.ValueString(), - Name: p.Domain.ValueString(), - Redirect: p.Redirect.ValueString(), - RedirectStatusCode: p.RedirectStatusCode.ValueInt64(), + GitBranch: p.GitBranch.ValueString(), + CustomEnvironmentID: p.CustomEnvironmentID.ValueString(), + Name: p.Domain.ValueString(), + Redirect: p.Redirect.ValueString(), + RedirectStatusCode: p.RedirectStatusCode.ValueInt64(), } } func (p *ProjectDomain) toUpdateRequest() client.UpdateProjectDomainRequest { return client.UpdateProjectDomainRequest{ - GitBranch: p.GitBranch.ValueStringPointer(), - Redirect: p.Redirect.ValueStringPointer(), - RedirectStatusCode: p.RedirectStatusCode.ValueInt64Pointer(), + GitBranch: p.GitBranch.ValueStringPointer(), + CustomEnvironmentID: p.CustomEnvironmentID.ValueStringPointer(), + Redirect: p.Redirect.ValueStringPointer(), + RedirectStatusCode: p.RedirectStatusCode.ValueInt64Pointer(), } } diff --git a/vercel/resource_project_domain_test.go b/vercel/resource_project_domain_test.go index 288deea9..090f7167 100644 --- a/vercel/resource_project_domain_test.go +++ b/vercel/resource_project_domain_test.go @@ -14,7 +14,6 @@ import ( ) func TestAcc_ProjectDomain(t *testing.T) { - t.Skip() testTeamID := resource.TestCheckNoResourceAttr("vercel_project.test", "team_id") if testTeam() != "" { testTeamID = resource.TestCheckResourceAttr("vercel_project.test", "team_id", testTeam()) @@ -30,32 +29,32 @@ func TestAcc_ProjectDomain(t *testing.T) { Steps: []resource.TestStep{ // Check error adding production domain { - Config: testAccProjectDomainWithProductionGitBranch(projectSuffix, domain, teamIDConfig()), + Config: testAccProjectDomainWithProductionGitBranch(projectSuffix, "1"+domain, teamIDConfig()), ExpectError: regexp.MustCompile( strings.ReplaceAll("the git_branch specified is the production branch. If you want to use this domain as a production domain, please omit the git_branch field.", " ", `\s*`), ), }, // Create and Read testing { - Config: testAccProjectDomainConfig(projectSuffix, domain, teamIDConfig()), + Config: testAccProjectDomainConfig(projectSuffix, "2"+domain, teamIDConfig()), Check: resource.ComposeAggregateTestCheckFunc( - testAccProjectDomainExists("vercel_project.test", testTeam(), domain), + testAccProjectDomainExists("vercel_project.test", testTeam(), "2"+domain), testTeamID, - resource.TestCheckResourceAttr("vercel_project_domain.test", "domain", domain), + resource.TestCheckResourceAttr("vercel_project_domain.test", "domain", "2"+domain), ), }, // Update testing { - Config: testAccProjectDomainConfigUpdated(projectSuffix, domain, teamIDConfig()), + Config: testAccProjectDomainConfigUpdated(projectSuffix, "2"+domain, teamIDConfig()), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("vercel_project_domain.test", "redirect", "test-acc-domain.vercel.app"), + resource.TestCheckResourceAttrSet("vercel_project_domain.test", "redirect"), ), }, // Redirect Update testing { - Config: testAccProjectDomainConfigUpdated2(projectSuffix, domain, teamIDConfig()), + Config: testAccProjectDomainConfigUpdated2(projectSuffix, "2"+domain, teamIDConfig()), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("vercel_project_domain.test", "redirect", "test-acc-domain.vercel.app"), + resource.TestCheckResourceAttrSet("vercel_project_domain.test", "redirect"), resource.TestCheckResourceAttr("vercel_project_domain.test", "redirect_status_code", "307"), ), }, @@ -66,7 +65,30 @@ func TestAcc_ProjectDomain(t *testing.T) { }, }, }) +} +func TestAcc_ProjectDomainCustomEnvironment(t *testing.T) { + randomSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: noopDestroyCheck, + Steps: []resource.TestStep{ + // Ensure we can't have both git_branch and custom_environment_id + { + Config: testAccProjectDomainConfigWithCustomEnvironmentAndGitBranch(randomSuffix), + ExpectError: regexp.MustCompile( + strings.ReplaceAll("Attribute \"git_branch\" cannot be specified when \"custom_environment_id\" is specified", " ", `\s*`), + ), + }, + { + Config: testAccProjectDomainConfigWithCustomEnvironment(randomSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project_domain.test", "custom_environment_id"), + ), + }, + }, + }) } func testAccProjectDomainExists(n, teamID, domain string) resource.TestCheckFunc { @@ -154,7 +176,13 @@ resource "vercel_project_domain" "test" { project_id = vercel_project.test.id %s - redirect = "test-acc-domain.vercel.app" + redirect = vercel_project_domain.redirect_target.domain +} + +resource "vercel_project_domain" "redirect_target" { + domain = "redirect-target-1-%[3]s" + project_id = vercel_project.test.id + %[2]s } `, projectSuffix, extra, domain, extra) } @@ -162,19 +190,31 @@ resource "vercel_project_domain" "test" { func testAccProjectDomainConfigUpdated2(projectSuffix, domain, extra string) string { return fmt.Sprintf(` resource "vercel_project" "test" { - name = "test-acc-domain-%s" - %s + name = "test-acc-domain-%[1]s" + %[2]s +} + +resource "vercel_project_domain" "redirect_target" { + domain = "redirect-target-1-%[3]s" + project_id = vercel_project.test.id + %[2]s +} + +resource "vercel_project_domain" "redirect_target_2" { + domain = "redirect-target-2-%[3]s" + project_id = vercel_project.test.id + %[2]s } resource "vercel_project_domain" "test" { - domain = "%s" + domain = "%[3]s" project_id = vercel_project.test.id - %s + %[2]s - redirect = "test-acc-domain.vercel.app" + redirect = vercel_project_domain.redirect_target_2.domain redirect_status_code = 307 } -`, projectSuffix, extra, domain, extra) +`, projectSuffix, extra, domain) } func testAccProjectDomainConfigDeleted(projectSuffix, extra string) string { @@ -185,3 +225,52 @@ resource "vercel_project" "test" { } `, projectSuffix, extra) } + +func testAccProjectDomainConfigWithCustomEnvironment(randomSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + name = "test-acc-domain-%[1]s" + %[2]s +} + +resource "vercel_custom_environment" "test" { + name = "test-acc-custom-environment" + project_id = vercel_project.test.id + %[2]s +} + +resource "vercel_project_domain" "test" { + domain = "test-acc-domain-%[1]s-foobar.vercel.app" + project_id = vercel_project.test.id + custom_environment_id = vercel_custom_environment.test.id + %[2]s +} +`, randomSuffix, teamIDConfig()) +} + +func testAccProjectDomainConfigWithCustomEnvironmentAndGitBranch(randomSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + name = "test-acc-domain-%[1]s" + %[2]s + git_repository = { + type = "github" + repo = "%[3]s" + } +} + +resource "vercel_custom_environment" "test" { + name = "test-acc-custom-environment" + project_id = vercel_project.test.id + %[2]s +} + +resource "vercel_project_domain" "test" { + domain = "test-acc-domain-%[1]s.vercel.app" + project_id = vercel_project.test.id + custom_environment_id = vercel_custom_environment.test.id + git_branch = "staging" + %[2]s +} +`, randomSuffix, teamIDConfig(), testGithubRepo()) +} From d65870386cb1781a0e5bfdf31292811b316030be Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Wed, 18 Dec 2024 12:49:01 +0000 Subject: [PATCH 4/9] Revert "Support environment variable targets for custom environments" This reverts commit e9fa7dc5eba271fbf89db2238b5eda66a5cb4654. --- vercel/data_source_shared_environment_variable.go | 6 +++++- vercel/resource_custom_environment_test.go | 8 ++++---- vercel/resource_project.go | 3 ++- vercel/resource_project_environment_variable.go | 3 ++- vercel/resource_project_environment_variables.go | 3 ++- vercel/resource_shared_environment_variable.go | 3 ++- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/vercel/data_source_shared_environment_variable.go b/vercel/data_source_shared_environment_variable.go index 2f8aa31e..e2924ce3 100644 --- a/vercel/data_source_shared_environment_variable.go +++ b/vercel/data_source_shared_environment_variable.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -106,9 +107,12 @@ For more detailed information, please see the [Vercel documentation](https://ver "target": schema.SetAttribute{ Optional: true, Computed: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", ElementType: types.StringType, Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOf("production", "preview", "development"), + ), setvalidator.SizeAtLeast(1), }, }, diff --git a/vercel/resource_custom_environment_test.go b/vercel/resource_custom_environment_test.go index 84efaae1..c562328c 100644 --- a/vercel/resource_custom_environment_test.go +++ b/vercel/resource_custom_environment_test.go @@ -50,7 +50,7 @@ func TestAcc_CustomEnvironmentResource(t *testing.T) { testCheckCustomEnvironmentExists("vercel_custom_environment.test"), resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "id"), resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "project_id"), - resource.TestCheckResourceAttr("vercel_custom_environment.test", "name", "test-acc"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test", "name"), resource.TestCheckNoResourceAttr("vercel_custom_environment.test", "branch_tracking"), resource.TestCheckResourceAttr("vercel_custom_environment.test", "description", "without branch tracking"), @@ -118,7 +118,7 @@ resource "vercel_project" "test" { resource "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc" + name = "test-acc-%[1]s" description = "without branch tracking" } @@ -144,7 +144,7 @@ resource "vercel_shared_environment_variable" "test" { resource "vercel_custom_environment" "test_bt" { project_id = vercel_project.test.id %[2]s - name = "test-acc-bt" + name = "test-acc-bt-%[1]s" description = "with branch tracking" branch_tracking = { pattern = "staging-" @@ -165,7 +165,7 @@ resource "vercel_project" "test" { resource "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc-updated" + name = "test-acc-%[1]s-updated" description = "without branch tracking updated" branch_tracking = { pattern = "staging-" diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 2c41b099..f21251b6 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -124,9 +124,10 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ - Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", ElementType: types.StringType, Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, Required: true, diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index be82cf2e..aaf7eaaf 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -71,9 +71,10 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", ElementType: types.StringType, Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, }, diff --git a/vercel/resource_project_environment_variables.go b/vercel/resource_project_environment_variables.go index afac9381..015ee57e 100644 --- a/vercel/resource_project_environment_variables.go +++ b/vercel/resource_project_environment_variables.go @@ -101,9 +101,10 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ }, "target": schema.SetAttribute{ Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", ElementType: types.StringType, Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, }, diff --git a/vercel/resource_shared_environment_variable.go b/vercel/resource_shared_environment_variable.go index 65c77cc1..f369e43d 100644 --- a/vercel/resource_shared_environment_variable.go +++ b/vercel/resource_shared_environment_variable.go @@ -113,9 +113,10 @@ For more detailed information, please see the [Vercel documentation](https://ver Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are `production`, `preview`, `development`, or the name of a `vercel_custom_enviroment`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", ElementType: types.StringType, Validators: []validator.Set{ + setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), }, }, From e7083ea2602bf0d6db1fc2c879a066055384f127 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Wed, 18 Dec 2024 16:46:42 +0000 Subject: [PATCH 5/9] Fleshing out environment stuff --- client/custom_environment.go | 3 +- client/environment_variable.go | 42 ++-- client/project.go | 21 +- vercel/resource_custom_environment.go | 9 +- vercel/resource_custom_environment_test.go | 42 ++-- vercel/resource_project.go | 166 ++++++++----- .../resource_project_environment_variable.go | 152 ++++++++---- .../resource_project_environment_variables.go | 223 ++++++++++-------- vercel/resource_project_members.go | 3 + 9 files changed, 406 insertions(+), 255 deletions(-) diff --git a/client/custom_environment.go b/client/custom_environment.go index 99a0c5cc..53df075e 100644 --- a/client/custom_environment.go +++ b/client/custom_environment.go @@ -84,13 +84,14 @@ func (c *Client) GetCustomEnvironment(ctx context.Context, request GetCustomEnvi type UpdateCustomEnvironmentRequest struct { TeamID string `json:"-"` ProjectID string `json:"-"` + OldSlug string `json:"-"` // Needed to get the right URL Slug string `json:"slug"` Description string `json:"description"` BranchMatcher *BranchMatcher `json:"branchMatcher"` } func (c *Client) UpdateCustomEnvironment(ctx context.Context, request UpdateCustomEnvironmentRequest) (res CustomEnvironmentResponse, err error) { - url := fmt.Sprintf("%s/v1/projects/%s/custom-environments/%s", c.baseURL, request.ProjectID, request.Slug) + url := fmt.Sprintf("%s/v1/projects/%s/custom-environments/%s", c.baseURL, request.ProjectID, request.OldSlug) if c.teamID(request.TeamID) != "" { url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) } diff --git a/client/environment_variable.go b/client/environment_variable.go index 093daced..e97fba04 100644 --- a/client/environment_variable.go +++ b/client/environment_variable.go @@ -10,12 +10,13 @@ import ( // CreateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to // create an environment variable. type EnvironmentVariableRequest struct { - Key string `json:"key"` - Value string `json:"value"` - Target []string `json:"target"` - GitBranch *string `json:"gitBranch,omitempty"` - Type string `json:"type"` - Comment string `json:"comment"` + Key string `json:"key"` + Value string `json:"value"` + Target []string `json:"target,omitempty"` + CustomEnvironmentIDs []string `json:"customEnvironmentIds,omitempty"` + GitBranch *string `json:"gitBranch,omitempty"` + Type string `json:"type"` + Comment string `json:"comment"` } type CreateEnvironmentVariableRequest struct { @@ -58,7 +59,7 @@ func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEn } } if err != nil { - return e, err + return e, fmt.Errorf("%w - %s", err, payload) } // The API response returns an encrypted environment variable, but we want to return the decrypted version. e.Value = request.EnvironmentVariable.Value @@ -127,6 +128,14 @@ type CreateEnvironmentVariablesResponse struct { } func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateEnvironmentVariablesRequest) ([]EnvironmentVariable, error) { + if len(request.EnvironmentVariables) == 1 { + env, err := c.CreateEnvironmentVariable(ctx, CreateEnvironmentVariableRequest{ + EnvironmentVariable: request.EnvironmentVariables[0], + ProjectID: request.ProjectID, + TeamID: request.TeamID, + }) + return []EnvironmentVariable{env}, err + } 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)) @@ -145,7 +154,7 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE body: payload, }, &response) if err != nil { - return nil, err + return nil, fmt.Errorf("%w - %s", err, payload) } if len(response.Failed) > 0 { @@ -176,14 +185,15 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE // UpdateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to // update an environment variable. type UpdateEnvironmentVariableRequest struct { - Value string `json:"value"` - Target []string `json:"target"` - GitBranch *string `json:"gitBranch,omitempty"` - Type string `json:"type"` - Comment string `json:"comment"` - ProjectID string `json:"-"` - TeamID string `json:"-"` - EnvID string `json:"-"` + Value string `json:"value"` + Target []string `json:"target"` + CustomEnvironmentIDs []string `json:"customEnvironmentIds,omitempty"` + GitBranch *string `json:"gitBranch,omitempty"` + Type string `json:"type"` + Comment string `json:"comment"` + ProjectID string `json:"-"` + TeamID string `json:"-"` + EnvID string `json:"-"` } // UpdateEnvironmentVariable will update an existing environment variable to the latest information. diff --git a/client/project.go b/client/project.go index db33a540..f16d0cf2 100644 --- a/client/project.go +++ b/client/project.go @@ -23,15 +23,16 @@ type OIDCTokenConfig struct { // EnvironmentVariable defines the information Vercel requires and surfaces about an environment variable // that is associated with a project. type EnvironmentVariable struct { - Key string `json:"key"` - Value string `json:"value"` - Target []string `json:"target"` - GitBranch *string `json:"gitBranch,omitempty"` - Type string `json:"type"` - ID string `json:"id,omitempty"` - TeamID string `json:"-"` - Comment string `json:"comment"` - Decrypted bool `json:"decrypted"` + Key string `json:"key"` + Value string `json:"value"` + Target []string `json:"target"` + CustomEnvironmentIDs []string `json:"customEnvironmentIds"` + GitBranch *string `json:"gitBranch,omitempty"` + Type string `json:"type"` + ID string `json:"id,omitempty"` + TeamID string `json:"-"` + Comment string `json:"comment"` + Decrypted bool `json:"decrypted"` } type DeploymentExpiration struct { @@ -46,7 +47,7 @@ type CreateProjectRequest struct { BuildCommand *string `json:"buildCommand"` CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep,omitempty"` DevCommand *string `json:"devCommand"` - EnvironmentVariables []EnvironmentVariable `json:"environmentVariables"` + EnvironmentVariables []EnvironmentVariable `json:"environmentVariables,omitempty"` Framework *string `json:"framework"` GitRepository *GitRepository `json:"gitRepository,omitempty"` InstallCommand *string `json:"installCommand"` diff --git a/vercel/resource_custom_environment.go b/vercel/resource_custom_environment.go index 97398f9d..6554d5a2 100644 --- a/vercel/resource_custom_environment.go +++ b/vercel/resource_custom_environment.go @@ -163,7 +163,7 @@ func (c CustomEnvironment) toCreateRequest(ctx context.Context) (client.CreateCu }, nil } -func (c CustomEnvironment) toUpdateRequest(ctx context.Context) (client.UpdateCustomEnvironmentRequest, diag.Diagnostics) { +func (c CustomEnvironment) toUpdateRequest(ctx context.Context, name string) (client.UpdateCustomEnvironmentRequest, diag.Diagnostics) { var bm *client.BranchMatcher if !c.BranchTracking.IsNull() && !c.BranchTracking.IsUnknown() { bt, diags := c.branchTracking(ctx) @@ -178,9 +178,10 @@ func (c CustomEnvironment) toUpdateRequest(ctx context.Context) (client.UpdateCu return client.UpdateCustomEnvironmentRequest{ TeamID: c.TeamID.ValueString(), ProjectID: c.ProjectID.ValueString(), - Slug: c.Name.ValueString(), + Slug: c.Name.ValueString(), // name is the slug Description: c.Description.ValueString(), BranchMatcher: bm, + OldSlug: name, }, nil } @@ -288,11 +289,13 @@ func (r *customEnvironmentResource) Read(ctx context.Context, req resource.ReadR func (r *customEnvironmentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var plan CustomEnvironment resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + var state CustomEnvironment + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - updateRequest, diags := plan.toUpdateRequest(ctx) + updateRequest, diags := plan.toUpdateRequest(ctx, state.Name.ValueString()) if diags.HasError() { resp.Diagnostics.Append(diags...) return diff --git a/vercel/resource_custom_environment_test.go b/vercel/resource_custom_environment_test.go index c562328c..09d7388a 100644 --- a/vercel/resource_custom_environment_test.go +++ b/vercel/resource_custom_environment_test.go @@ -54,21 +54,19 @@ func TestAcc_CustomEnvironmentResource(t *testing.T) { resource.TestCheckNoResourceAttr("vercel_custom_environment.test", "branch_tracking"), resource.TestCheckResourceAttr("vercel_custom_environment.test", "description", "without branch tracking"), - /* - testCheckCustomEnvironmentExists("vercel_custom_environment.test_bt"), - resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "id"), - resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "project_id"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "name", "test-acc-bt"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.type", "startsWith"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.pattern", "staging-"), - resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "description", "with branch tracking"), - */ + testCheckCustomEnvironmentExists("vercel_custom_environment.test_bt"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "project_id"), + resource.TestCheckResourceAttrSet("vercel_custom_environment.test_bt", "name"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.type", "startsWith"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "branch_tracking.pattern", "staging-"), + resource.TestCheckResourceAttr("vercel_custom_environment.test_bt", "description", "with branch tracking"), // check project env var - resource.TestCheckTypeSetElemAttr("vercel_project_environment_variable.test", "target.*", "test-acc"), + resource.TestCheckResourceAttr("vercel_project_environment_variable.test", "custom_environment_ids.#", "1"), // check shared env var - resource.TestCheckTypeSetElemAttr("vercel_shared_environment_variable.test", "target.*", "test-acc"), + resource.TestCheckResourceAttr("vercel_project_environment_variables.test", "variables.0.custom_environment_ids.#", "1"), ), }, { @@ -87,7 +85,7 @@ func TestAcc_CustomEnvironmentResource(t *testing.T) { ResourceName: "vercel_custom_environment.test", ImportState: true, ImportStateVerify: true, - ImportStateIdFunc: getCustomEnvImportID("vercel_shared_environment_variable.example"), + ImportStateIdFunc: getCustomEnvImportID("vercel_custom_environment.test"), }, }, }) @@ -128,19 +126,20 @@ resource "vercel_project_environment_variable" "test" { %[2]s key = "foo" value = "test-acc-env-var" - target = [vercel_custom_environment.test.name] + custom_environment_ids = [vercel_custom_environment.test.id] } -// Ensurer shared_environment_variable works -resource "vercel_shared_environment_variable" "test" { - project_ids = [vercel_project.test.id] +// Ensure project_environment_variables works +resource "vercel_project_environment_variables" "test" { + project_id = vercel_project.test.id %[2]s - key = "bar" - value = "test-acc-shared-env-var" - target = [vercel_custom_environment.test.name] + variables = [{ + key = "bar" + value = "test-acc-env-var" + custom_environment_ids = [vercel_custom_environment.test.id] + }] } -/* resource "vercel_custom_environment" "test_bt" { project_id = vercel_project.test.id %[2]s @@ -151,7 +150,6 @@ resource "vercel_custom_environment" "test_bt" { type = "startsWith" } } -*/ `, projectSuffix, teamIDConfig()) } @@ -165,7 +163,7 @@ resource "vercel_project" "test" { resource "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc-%[1]s-updated" + name = "test-acc-%[1]s-updtd" description = "without branch tracking updated" branch_tracking = { pattern = "staging-" diff --git a/vercel/resource_project.go b/vercel/resource_project.go index f21251b6..86f8b99b 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -124,13 +124,37 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. Cannot be set in conjuction with custom_environment_ids", ElementType: types.StringType, Validators: []validator.Set{ setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), + setvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("target"), + path.MatchRelative().AtParent().AtName("custom_environment_ids"), + ), + }, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + }, + "custom_environment_ids": schema.SetAttribute{ + Description: "The IDs of Custom Environments that the Environment Variable should be present on. Cannot be set in conjuction with `target`.", + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("target"), + path.MatchRelative().AtParent().AtName("custom_environment_ids"), + ), + }, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), }, - Required: true, }, "git_branch": schema.StringAttribute{ Description: "The git branch of the Environment Variable.", @@ -642,16 +666,20 @@ func (p *Project) environment(ctx context.Context) ([]EnvironmentItem, error) { return vars, nil } -func parseEnvironment(vars []EnvironmentItem) []client.EnvironmentVariable { - out := []client.EnvironmentVariable{} +func parseEnvironment(ctx context.Context, vars []EnvironmentItem) (out []client.EnvironmentVariable, diags diag.Diagnostics) { for _, e := range vars { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) + var target []string + diags = e.Target.ElementsAs(ctx, &target, false) + if diags.HasError() { + return out, diags + } + var customEnvironmentIDs []string + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, false) + if diags.HasError() { + return out, diags } var envVariableType string - if e.Sensitive.ValueBool() { envVariableType = "sensitive" } else { @@ -659,24 +687,26 @@ func parseEnvironment(vars []EnvironmentItem) []client.EnvironmentVariable { } out = append(out, client.EnvironmentVariable{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - GitBranch: e.GitBranch.ValueStringPointer(), - Type: envVariableType, - ID: e.ID.ValueString(), - Comment: e.Comment.ValueString(), + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + CustomEnvironmentIDs: customEnvironmentIDs, + GitBranch: e.GitBranch.ValueStringPointer(), + Type: envVariableType, + ID: e.ID.ValueString(), + Comment: e.Comment.ValueString(), }) } - return out + return out, nil } -func (p *Project) toCreateProjectRequest(envs []EnvironmentItem) client.CreateProjectRequest { +func (p *Project) toCreateProjectRequest(ctx context.Context, envs []EnvironmentItem) (req client.CreateProjectRequest, diags diag.Diagnostics) { + clientEnvs, diags := parseEnvironment(ctx, envs) return client.CreateProjectRequest{ BuildCommand: p.BuildCommand.ValueStringPointer(), CommandForIgnoringBuildStep: p.IgnoreCommand.ValueStringPointer(), DevCommand: p.DevCommand.ValueStringPointer(), - EnvironmentVariables: parseEnvironment(envs), + EnvironmentVariables: clientEnvs, Framework: p.Framework.ValueStringPointer(), GitRepository: p.GitRepository.toCreateProjectRequest(), InstallCommand: p.InstallCommand.ValueStringPointer(), @@ -686,7 +716,7 @@ func (p *Project) toCreateProjectRequest(envs []EnvironmentItem) client.CreatePr PublicSource: p.PublicSource.ValueBoolPointer(), RootDirectory: p.RootDirectory.ValueStringPointer(), ServerlessFunctionRegion: p.ServerlessFunctionRegion.ValueString(), - } + }, diags } func toSkewProtectionAge(sp types.String) int { @@ -749,23 +779,29 @@ func (p *Project) toUpdateProjectRequest(ctx context.Context, oldName string) (r // EnvironmentItem reflects the state terraform stores internally for a project's environment variable. type EnvironmentItem struct { - Target []types.String `tfsdk:"target"` - GitBranch types.String `tfsdk:"git_branch"` - Key types.String `tfsdk:"key"` - Value types.String `tfsdk:"value"` - ID types.String `tfsdk:"id"` - Sensitive types.Bool `tfsdk:"sensitive"` - Comment types.String `tfsdk:"comment"` + Target types.Set `tfsdk:"target"` + CustomEnvironmentIDs types.Set `tfsdk:"custom_environment_ids"` + GitBranch types.String `tfsdk:"git_branch"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` + Comment types.String `tfsdk:"comment"` } -func (e *EnvironmentItem) toEnvironmentVariableRequest() client.EnvironmentVariableRequest { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) +func (e *EnvironmentItem) toEnvironmentVariableRequest(ctx context.Context) (req client.EnvironmentVariableRequest, diags diag.Diagnostics) { + var target []string + diags = e.Target.ElementsAs(ctx, &target, false) + if diags.HasError() { + return req, diags + } + var customEnvironmentIDs []string + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, false) + if diags.HasError() { + return req, diags } var envVariableType string - if e.Sensitive.ValueBool() { envVariableType = "sensitive" } else { @@ -773,13 +809,14 @@ func (e *EnvironmentItem) toEnvironmentVariableRequest() client.EnvironmentVaria } return client.EnvironmentVariableRequest{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - GitBranch: e.GitBranch.ValueStringPointer(), - Type: envVariableType, - Comment: e.Comment.ValueString(), - } + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + CustomEnvironmentIDs: customEnvironmentIDs, + GitBranch: e.GitBranch.ValueStringPointer(), + Type: envVariableType, + Comment: e.Comment.ValueString(), + }, nil } type DeployHook struct { @@ -1032,6 +1069,9 @@ var envVariableElemType = types.ObjectType{ "target": types.SetType{ ElemType: types.StringType, }, + "custom_environment_ids": types.SetType{ + ElemType: types.StringType, + }, "git_branch": types.StringType, "id": types.StringType, "sensitive": types.BoolType, @@ -1044,13 +1084,12 @@ var gitCommentsAttrTypes = map[string]attr.Type{ "on_pull_request": types.BoolType, } -func hasSameTarget(p EnvironmentItem, target []string) bool { - if len(p.Target) != len(target) { +func isSameStringSet(a []string, b []string) bool { + if len(a) != len(b) { return false } - for _, t := range p.Target { - v := t.ValueString() - if !contains(target, v) { + for _, v := range a { + if !contains(b, v) { return false } } @@ -1201,6 +1240,10 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon for _, t := range e.Target { target = append(target, types.StringValue(t)) } + var customEnvironmentIDs []attr.Value + for _, c := range e.CustomEnvironmentIDs { + customEnvironmentIDs = append(customEnvironmentIDs, types.StringValue(c)) + } value := types.StringValue(e.Value) if e.Type == "sensitive" { value = types.StringNull() @@ -1209,8 +1252,12 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon return Project{}, fmt.Errorf("error reading project environment variables: %s", err) } for _, p := range environment { - if p.Key.ValueString() == e.Key && hasSameTarget(p, e.Target) { - + var target []string + diags := p.Target.ElementsAs(ctx, &target, false) + if diags.HasError() { + return Project{}, fmt.Errorf("error reading project environment variables: %s", diags) + } + if p.Key.ValueString() == e.Key && isSameStringSet(target, e.Target) { value = p.Value break } @@ -1218,13 +1265,14 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon } env = append(env, types.ObjectValueMust(envVariableElemType.AttrTypes, map[string]attr.Value{ - "key": types.StringValue(e.Key), - "value": value, - "target": types.SetValueMust(types.StringType, target), - "git_branch": types.StringPointerValue(e.GitBranch), - "id": types.StringValue(e.ID), - "sensitive": types.BoolValue(e.Type == "sensitive"), - "comment": types.StringValue(e.Comment), + "key": types.StringValue(e.Key), + "value": value, + "target": types.SetValueMust(types.StringType, target), + "custom_environment_ids": types.SetValueMust(types.StringType, customEnvironmentIDs), + "git_branch": types.StringPointerValue(e.GitBranch), + "id": types.StringValue(e.ID), + "sensitive": types.BoolValue(e.Type == "sensitive"), + "comment": types.StringValue(e.Comment), })) } @@ -1383,7 +1431,12 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - out, err := r.client.CreateProject(ctx, plan.TeamID.ValueString(), plan.toCreateProjectRequest(environment)) + request, diags := plan.toCreateProjectRequest(ctx, environment) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + out, err := r.client.CreateProject(ctx, plan.TeamID.ValueString(), request) if err != nil { resp.Diagnostics.AddError( "Error creating project", @@ -1749,7 +1802,12 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest var items []client.EnvironmentVariableRequest for _, v := range toCreate { - items = append(items, v.toEnvironmentVariableRequest()) + vv, diags := v.toEnvironmentVariableRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + items = append(items, vv) } if items != nil { diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index aaf7eaaf..30ffb95d 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -6,11 +6,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "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/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -70,12 +73,34 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ `, Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ - Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. Cannot be set in conjuction with custom_environment_ids", ElementType: types.StringType, Validators: []validator.Set{ setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), + setvalidator.ExactlyOneOf( + path.MatchRoot("custom_environment_ids"), + path.MatchRoot("target"), + ), + }, + }, + "custom_environment_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: "The IDs of Custom Environments that the Environment Variable should be present on. Cannot be set in conjuction with `target`.", + PlanModifiers: []planmodifier.Set{setplanmodifier.RequiresReplace(), setplanmodifier.UseStateForUnknown()}, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.ExactlyOneOf( + path.MatchRoot("custom_environment_ids"), + path.MatchRoot("target"), + ), }, }, "key": schema.StringAttribute{ @@ -129,15 +154,16 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ // ProjectEnvironmentVariable reflects the state terraform stores internally for a project environment variable. type ProjectEnvironmentVariable struct { - Target []types.String `tfsdk:"target"` - GitBranch types.String `tfsdk:"git_branch"` - Key types.String `tfsdk:"key"` - Value types.String `tfsdk:"value"` - TeamID types.String `tfsdk:"team_id"` - ProjectID types.String `tfsdk:"project_id"` - ID types.String `tfsdk:"id"` - Sensitive types.Bool `tfsdk:"sensitive"` - Comment types.String `tfsdk:"comment"` + Target types.Set `tfsdk:"target"` + CustomEnvironmentIDs types.Set `tfsdk:"custom_environment_ids"` + GitBranch types.String `tfsdk:"git_branch"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + TeamID types.String `tfsdk:"team_id"` + ProjectID types.String `tfsdk:"project_id"` + ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` + Comment types.String `tfsdk:"comment"` } func (r *projectEnvironmentVariableResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { @@ -183,13 +209,18 @@ func (r *projectEnvironmentVariableResource) ModifyPlan(ctx context.Context, req ) } -func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest() client.CreateEnvironmentVariableRequest { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) +func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest(ctx context.Context) (req client.CreateEnvironmentVariableRequest, diags diag.Diagnostics) { + var target []string + diags = e.Target.ElementsAs(ctx, &target, true) + if diags.HasError() { + return req, diags + } + var customEnvironmentIDs []string + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) + if diags.HasError() { + return req, diags } var envVariableType string - if e.Sensitive.ValueBool() { envVariableType = "sensitive" } else { @@ -198,26 +229,32 @@ func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest() client return client.CreateEnvironmentVariableRequest{ EnvironmentVariable: client.EnvironmentVariableRequest{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - GitBranch: e.GitBranch.ValueStringPointer(), - Type: envVariableType, - Comment: e.Comment.ValueString(), + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + CustomEnvironmentIDs: customEnvironmentIDs, + GitBranch: e.GitBranch.ValueStringPointer(), + Type: envVariableType, + Comment: e.Comment.ValueString(), }, ProjectID: e.ProjectID.ValueString(), TeamID: e.TeamID.ValueString(), - } + }, nil } -func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest() client.UpdateEnvironmentVariableRequest { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) +func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest(ctx context.Context) (r client.UpdateEnvironmentVariableRequest, diags diag.Diagnostics) { + var target []string + diags = e.Target.ElementsAs(ctx, &target, true) + if diags.HasError() { + return r, diags + } + var customEnvironmentIDs []string + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) + if diags.HasError() { + return r, diags } var envVariableType string - if e.Sensitive.ValueBool() { envVariableType = "sensitive" } else { @@ -225,22 +262,23 @@ func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest() client } return client.UpdateEnvironmentVariableRequest{ - Value: e.Value.ValueString(), - Target: target, - GitBranch: e.GitBranch.ValueStringPointer(), - Type: envVariableType, - ProjectID: e.ProjectID.ValueString(), - TeamID: e.TeamID.ValueString(), - EnvID: e.ID.ValueString(), - Comment: e.Comment.ValueString(), - } + Value: e.Value.ValueString(), + Target: target, + CustomEnvironmentIDs: customEnvironmentIDs, + GitBranch: e.GitBranch.ValueStringPointer(), + Type: envVariableType, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + EnvID: e.ID.ValueString(), + Comment: e.Comment.ValueString(), + }, nil } // 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, v types.String) ProjectEnvironmentVariable { - target := []types.String{} + var target []attr.Value for _, t := range response.Target { target = append(target, types.StringValue(t)) } @@ -250,16 +288,22 @@ func convertResponseToProjectEnvironmentVariable(response client.EnvironmentVari value = v } + var customEnvironmentIDs []attr.Value + for _, c := range response.CustomEnvironmentIDs { + customEnvironmentIDs = append(customEnvironmentIDs, types.StringValue(c)) + } + return ProjectEnvironmentVariable{ - Target: target, - GitBranch: types.StringPointerValue(response.GitBranch), - Key: types.StringValue(response.Key), - Value: value, - TeamID: toTeamID(response.TeamID), - ProjectID: projectID, - ID: types.StringValue(response.ID), - Sensitive: types.BoolValue(response.Type == "sensitive"), - Comment: types.StringValue(response.Comment), + Target: types.SetValueMust(types.StringType, target), + CustomEnvironmentIDs: types.SetValueMust(types.StringType, customEnvironmentIDs), + GitBranch: types.StringPointerValue(response.GitBranch), + Key: types.StringValue(response.Key), + Value: value, + TeamID: toTeamID(response.TeamID), + ProjectID: projectID, + ID: types.StringValue(response.ID), + Sensitive: types.BoolValue(response.Type == "sensitive"), + Comment: types.StringValue(response.Comment), } } @@ -282,7 +326,12 @@ func (r *projectEnvironmentVariableResource) Create(ctx context.Context, req res return } - response, err := r.client.CreateEnvironmentVariable(ctx, plan.toCreateEnvironmentVariableRequest()) + request, diags := plan.toCreateEnvironmentVariableRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + response, err := r.client.CreateEnvironmentVariable(ctx, request) if err != nil { resp.Diagnostics.AddError( "Error creating project environment variable", @@ -357,7 +406,12 @@ func (r *projectEnvironmentVariableResource) Update(ctx context.Context, req res return } - response, err := r.client.UpdateEnvironmentVariable(ctx, plan.toUpdateEnvironmentVariableRequest()) + request, diags := plan.toUpdateEnvironmentVariableRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + response, err := r.client.UpdateEnvironmentVariable(ctx, request) if err != nil { resp.Diagnostics.AddError( "Error updating project environment variable", diff --git a/vercel/resource_project_environment_variables.go b/vercel/resource_project_environment_variables.go index 015ee57e..58ec6a3e 100644 --- a/vercel/resource_project_environment_variables.go +++ b/vercel/resource_project_environment_variables.go @@ -7,11 +7,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "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/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -86,8 +88,9 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "The ID of the Environment Variable.", - Computed: true, + Description: "The ID of the Environment Variable.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Computed: true, }, "key": schema.StringAttribute{ Required: true, @@ -100,12 +103,32 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ // Sensitive: true, }, "target": schema.SetAttribute{ - Required: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", - ElementType: types.StringType, + Optional: true, + Computed: true, + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", + PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}, + ElementType: types.StringType, Validators: []validator.Set{ setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), + setvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("custom_environment_ids"), + path.MatchRelative().AtParent().AtName("target"), + ), + }, + }, + "custom_environment_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: "The IDs of Custom Environments that the Environment Variable should be present on. Cannot be set in conjuction with `target`.", + PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("custom_environment_ids"), + path.MatchRelative().AtParent().AtName("target"), + ), }, }, "git_branch": schema.StringAttribute{ @@ -116,12 +139,13 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Description: "Whether the Environment Variable is sensitive or not.", Optional: true, Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace(), boolplanmodifier.UseStateForUnknown()}, }, "comment": schema.StringAttribute{ - Description: "A comment explaining what the environment variable is for.", - Optional: true, - Computed: true, + Description: "A comment explaining what the environment variable is for.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, Validators: []validator.String{ stringvalidator.LengthBetween(0, 1000), }, @@ -140,17 +164,14 @@ type ProjectEnvironmentVariables struct { Variables types.Set `tfsdk:"variables"` } -func (p *ProjectEnvironmentVariables) environment(ctx context.Context) ([]EnvironmentItem, error) { +func (p *ProjectEnvironmentVariables) environment(ctx context.Context) ([]EnvironmentItem, diag.Diagnostics) { if p.Variables.IsNull() { return nil, nil } var vars []EnvironmentItem - err := p.Variables.ElementsAs(ctx, &vars, true) - if err != nil { - return nil, fmt.Errorf("error reading project environment variables: %s", err) - } - return vars, nil + diags := p.Variables.ElementsAs(ctx, &vars, true) + return vars, diags } func (r *projectEnvironmentVariablesResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { @@ -164,12 +185,9 @@ func (r *projectEnvironmentVariablesResource) ModifyPlan(ctx context.Context, re return } - environment, err := config.environment(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + environment, diags := config.environment(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } @@ -224,17 +242,23 @@ func (r *projectEnvironmentVariablesResource) ModifyPlan(ctx context.Context, re } } -func (e *ProjectEnvironmentVariables) toCreateEnvironmentVariablesRequest(ctx context.Context) (r client.CreateEnvironmentVariablesRequest, err error) { - envs, err := e.environment(ctx) - if err != nil { - return r, err +func (e *ProjectEnvironmentVariables) toCreateEnvironmentVariablesRequest(ctx context.Context) (r client.CreateEnvironmentVariablesRequest, diags diag.Diagnostics) { + envs, diags := e.environment(ctx) + if diags.HasError() { + return r, diags } variables := []client.EnvironmentVariableRequest{} for _, e := range envs { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) + var target []string + diags = e.Target.ElementsAs(ctx, &target, true) + if diags.HasError() { + return r, diags + } + var customEnvironmentIDs []string + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) + if diags.HasError() { + return r, diags } var envVariableType string if e.Sensitive.ValueBool() { @@ -243,12 +267,13 @@ func (e *ProjectEnvironmentVariables) toCreateEnvironmentVariablesRequest(ctx co envVariableType = "encrypted" } variables = append(variables, client.EnvironmentVariableRequest{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - Type: envVariableType, - GitBranch: e.GitBranch.ValueStringPointer(), - Comment: e.Comment.ValueString(), + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + CustomEnvironmentIDs: customEnvironmentIDs, + Type: envVariableType, + GitBranch: e.GitBranch.ValueStringPointer(), + Comment: e.Comment.ValueString(), }) } @@ -262,31 +287,52 @@ func (e *ProjectEnvironmentVariables) toCreateEnvironmentVariablesRequest(ctx co // convertResponseToProjectEnvironmentVariables 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 convertResponseToProjectEnvironmentVariables(ctx context.Context, response []client.EnvironmentVariable, plan ProjectEnvironmentVariables) (ProjectEnvironmentVariables, error) { - environment, err := plan.environment(ctx) - if err != nil { - return ProjectEnvironmentVariables{}, fmt.Errorf("error reading project environment variables: %s", err) +func convertResponseToProjectEnvironmentVariables(ctx context.Context, response []client.EnvironmentVariable, plan ProjectEnvironmentVariables) (ProjectEnvironmentVariables, diag.Diagnostics) { + environment, diags := plan.environment(ctx) + if diags.HasError() { + return ProjectEnvironmentVariables{}, diags } var env []attr.Value + alreadyPresent := map[string]struct{}{} for _, e := range response { target := []attr.Value{} for _, t := range e.Target { target = append(target, types.StringValue(t)) } + var customEnvironmentIDs []attr.Value + for _, c := range e.CustomEnvironmentIDs { + customEnvironmentIDs = append(customEnvironmentIDs, types.StringValue(c)) + } value := types.StringValue(e.Value) if e.Type == "sensitive" { value = types.StringNull() } if !e.Decrypted || e.Type == "sensitive" { for _, p := range environment { - if p.Key.ValueString() == e.Key && hasSameTarget(p, e.Target) { + var target []string + diags := p.Target.ElementsAs(ctx, &target, true) + if diags.HasError() { + return ProjectEnvironmentVariables{}, diags + } + var customEnvironmentIDs []string + diags = p.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) + if diags.HasError() { + return ProjectEnvironmentVariables{}, diags + } + if p.Key.ValueString() == e.Key && isSameStringSet(target, e.Target) && isSameStringSet(customEnvironmentIDs, e.CustomEnvironmentIDs) { value = p.Value break } } } + // The Vercel API returns duplicate environment variables, so we need to filter them out. + if _, ok := alreadyPresent[e.ID]; ok { + continue + } + alreadyPresent[e.ID] = struct{}{} + env = append(env, types.ObjectValueMust( map[string]attr.Type{ "key": types.StringType, @@ -294,19 +340,23 @@ func convertResponseToProjectEnvironmentVariables(ctx context.Context, response "target": types.SetType{ ElemType: types.StringType, }, + "custom_environment_ids": types.SetType{ + ElemType: types.StringType, + }, "git_branch": types.StringType, "id": types.StringType, "sensitive": types.BoolType, "comment": types.StringType, }, map[string]attr.Value{ - "key": types.StringValue(e.Key), - "value": value, - "target": types.SetValueMust(types.StringType, target), - "git_branch": types.StringPointerValue(e.GitBranch), - "id": types.StringValue(e.ID), - "sensitive": types.BoolValue(e.Type == "sensitive"), - "comment": types.StringValue(e.Comment), + "key": types.StringValue(e.Key), + "value": value, + "target": types.SetValueMust(types.StringType, target), + "custom_environment_ids": types.SetValueMust(types.StringType, customEnvironmentIDs), + "git_branch": types.StringPointerValue(e.GitBranch), + "id": types.StringValue(e.ID), + "sensitive": types.BoolValue(e.Type == "sensitive"), + "comment": types.StringValue(e.Comment), }, )) } @@ -337,12 +387,9 @@ func (r *projectEnvironmentVariablesResource) Create(ctx context.Context, req re return } - request, err := plan.toCreateEnvironmentVariablesRequest(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error creating project environment variables", - "Could not create project environment variables request, unexpected error: "+err.Error(), - ) + request, diags := plan.toCreateEnvironmentVariablesRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } created, err := r.client.CreateEnvironmentVariables(ctx, request) @@ -353,12 +400,9 @@ func (r *projectEnvironmentVariablesResource) Create(ctx context.Context, req re ) } - result, err := convertResponseToProjectEnvironmentVariables(ctx, created, plan) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + result, diags := convertResponseToProjectEnvironmentVariables(ctx, created, plan) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } @@ -385,12 +429,9 @@ func (r *projectEnvironmentVariablesResource) Read(ctx context.Context, req reso return } - existing, err := state.environment(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + existing, diags := state.environment(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } existingIDs := map[string]struct{}{} @@ -424,12 +465,9 @@ func (r *projectEnvironmentVariablesResource) Read(ctx context.Context, req reso } } - result, err := convertResponseToProjectEnvironmentVariables(ctx, toUse, state) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + result, diags := convertResponseToProjectEnvironmentVariables(ctx, toUse, state) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } @@ -461,20 +499,14 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re return } - stateEnvs, err := state.environment(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + stateEnvs, diags := state.environment(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } - planEnvs, err := plan.environment(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + planEnvs, diags := plan.environment(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } plannedIDs := map[string]struct{}{} @@ -512,12 +544,9 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re }) } - request, err := plan.toCreateEnvironmentVariablesRequest(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error creating project environment variables", - "Could not create project environment variables request, unexpected error: "+err.Error(), - ) + request, diags := plan.toCreateEnvironmentVariablesRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } response, err := r.client.CreateEnvironmentVariables(ctx, request) @@ -529,12 +558,9 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re return } - result, err := convertResponseToProjectEnvironmentVariables(ctx, response, plan) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + result, diags := convertResponseToProjectEnvironmentVariables(ctx, response, plan) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } @@ -559,12 +585,9 @@ func (r *projectEnvironmentVariablesResource) Delete(ctx context.Context, req re return } - envs, err := state.environment(ctx) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing project environment variables", - "Could not read environment variables, unexpected error: "+err.Error(), - ) + envs, diags := state.environment(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) return } for _, v := range envs { diff --git a/vercel/resource_project_members.go b/vercel/resource_project_members.go index 6ad2d5ee..e6a3ed82 100644 --- a/vercel/resource_project_members.go +++ b/vercel/resource_project_members.go @@ -90,6 +90,7 @@ This, however, means config drift will not be detected for members that are adde }, Validators: []validator.String{ stringvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("user_id"), path.MatchRelative().AtParent().AtName("email"), path.MatchRelative().AtParent().AtName("username"), ), @@ -105,6 +106,7 @@ This, however, means config drift will not be detected for members that are adde Validators: []validator.String{ stringvalidator.ExactlyOneOf( path.MatchRelative().AtParent().AtName("user_id"), + path.MatchRelative().AtParent().AtName("email"), path.MatchRelative().AtParent().AtName("username"), ), }, @@ -120,6 +122,7 @@ This, however, means config drift will not be detected for members that are adde stringvalidator.ExactlyOneOf( path.MatchRelative().AtParent().AtName("user_id"), path.MatchRelative().AtParent().AtName("email"), + path.MatchRelative().AtParent().AtName("username"), ), }, }, From dc29397c0346fde241294968ce87eba4c007f4d1 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Thu, 19 Dec 2024 13:08:48 +0000 Subject: [PATCH 6/9] Hopefully fix everything --- docs/resources/project.md | 3 +- docs/resources/project_domain.md | 1 + .../resources/project_environment_variable.md | 3 +- .../project_environment_variables.md | 3 +- vercel/resource_project.go | 18 ++- .../resource_project_environment_variable.go | 8 +- .../resource_project_environment_variables.go | 108 +++++++++++------- vercel/resource_team_config.go | 2 + 8 files changed, 96 insertions(+), 50 deletions(-) diff --git a/docs/resources/project.md b/docs/resources/project.md index 242c288a..e5c678bc 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -97,14 +97,15 @@ resource "vercel_project" "example" { 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, Sensitive) The value of the Environment Variable. Optional: - `comment` (String) A comment explaining what the environment variable is for. +- `custom_environment_ids` (Set of String) The IDs of Custom Environments that the Environment Variable should be present on. At least one of `target` or `custom_environment_ids` must be set. - `git_branch` (String) The git branch of the Environment Variable. - `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. (May be affected by a [team-wide environment variable policy](https://vercel.com/docs/projects/environment-variables/sensitive-environment-variables#environment-variables-policy)) +- `target` (Set of String) The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. At least one of `target` or `custom_environment_ids` must be set. Read-Only: diff --git a/docs/resources/project_domain.md b/docs/resources/project_domain.md index 36f14583..509768be 100644 --- a/docs/resources/project_domain.md +++ b/docs/resources/project_domain.md @@ -51,6 +51,7 @@ resource "vercel_project_domain" "example_redirect" { ### Optional +- `custom_environment_id` (String) The name of the Custom Environment to link to the Project Domain. Deployments from this custom environment will be assigned the domain name. - `git_branch` (String) Git branch to link to the project domain. Deployments from this git branch will be assigned the domain name. - `redirect` (String) The domain name that serves as a target destination for redirects. - `redirect_status_code` (Number) The HTTP status code to use when serving as a redirect. diff --git a/docs/resources/project_environment_variable.md b/docs/resources/project_environment_variable.md index ee3a62ea..f6f8de47 100644 --- a/docs/resources/project_environment_variable.md +++ b/docs/resources/project_environment_variable.md @@ -73,14 +73,15 @@ resource "vercel_project_environment_variable" "example_sensitive" { - `key` (String) The name of the Environment Variable. - `project_id` (String) The ID of the Vercel project. -- `target` (Set of String) The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. - `value` (String, Sensitive) The value of the Environment Variable. ### Optional - `comment` (String) A comment explaining what the environment variable is for. +- `custom_environment_ids` (Set of String) The IDs of Custom Environments that the Environment Variable should be present on. At least one of `target` or `custom_environment_ids` must be set. - `git_branch` (String) The git branch of the Environment Variable. - `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. (May be affected by a [team-wide environment variable policy](https://vercel.com/docs/projects/environment-variables/sensitive-environment-variables#environment-variables-policy)) +- `target` (Set of String) The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. At least one of `target` or `custom_environment_ids` must be set. - `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 diff --git a/docs/resources/project_environment_variables.md b/docs/resources/project_environment_variables.md index 3265ad68..eb6330ca 100644 --- a/docs/resources/project_environment_variables.md +++ b/docs/resources/project_environment_variables.md @@ -75,14 +75,15 @@ resource "vercel_project_environment_variables" "example" { 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. +- `custom_environment_ids` (Set of String) The IDs of Custom Environments that the Environment Variable should be present on. At least one of `target` or `custom_environment_ids` must be set. - `git_branch` (String) The git branch of the Environment Variable. - `sensitive` (Boolean) Whether the Environment Variable is sensitive or not. +- `target` (Set of String) The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. At least one of `target` or `custom_environment_ids` must be set. Read-Only: diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 86f8b99b..f863ea38 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -124,12 +124,12 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "target": schema.SetAttribute{ - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. Cannot be set in conjuction with custom_environment_ids", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. At least one of `target` or `custom_environment_ids` must be set.", ElementType: types.StringType, Validators: []validator.Set{ setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), - setvalidator.ExactlyOneOf( + setvalidator.AtLeastOneOf( path.MatchRelative().AtParent().AtName("target"), path.MatchRelative().AtParent().AtName("custom_environment_ids"), ), @@ -141,11 +141,11 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ }, }, "custom_environment_ids": schema.SetAttribute{ - Description: "The IDs of Custom Environments that the Environment Variable should be present on. Cannot be set in conjuction with `target`.", + Description: "The IDs of Custom Environments that the Environment Variable should be present on. At least one of `target` or `custom_environment_ids` must be set.", ElementType: types.StringType, Validators: []validator.Set{ setvalidator.SizeAtLeast(1), - setvalidator.ExactlyOneOf( + setvalidator.AtLeastOneOf( path.MatchRelative().AtParent().AtName("target"), path.MatchRelative().AtParent().AtName("custom_environment_ids"), ), @@ -789,6 +789,16 @@ type EnvironmentItem struct { Comment types.String `tfsdk:"comment"` } +func (e *EnvironmentItem) equal(other *EnvironmentItem) bool { + return e.Key.ValueString() == other.Key.ValueString() && + e.Value.ValueString() == other.Value.ValueString() && + e.Target.Equal(other.Target) && + e.CustomEnvironmentIDs.Equal(other.CustomEnvironmentIDs) && + e.GitBranch.ValueString() == other.GitBranch.ValueString() && + e.Sensitive.ValueBool() == other.Sensitive.ValueBool() && + e.Comment.ValueString() == other.Comment.ValueString() +} + func (e *EnvironmentItem) toEnvironmentVariableRequest(ctx context.Context) (req client.EnvironmentVariableRequest, diags diag.Diagnostics) { var target []string diags = e.Target.ElementsAs(ctx, &target, false) diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index 30ffb95d..7555b1ae 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -78,12 +78,12 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ PlanModifiers: []planmodifier.Set{ setplanmodifier.UseStateForUnknown(), }, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. Cannot be set in conjuction with custom_environment_ids", + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. At least one of `target` or `custom_environment_ids` must be set.", ElementType: types.StringType, Validators: []validator.Set{ setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), - setvalidator.ExactlyOneOf( + setvalidator.AtLeastOneOf( path.MatchRoot("custom_environment_ids"), path.MatchRoot("target"), ), @@ -93,11 +93,11 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Optional: true, Computed: true, ElementType: types.StringType, - Description: "The IDs of Custom Environments that the Environment Variable should be present on. Cannot be set in conjuction with `target`.", + Description: "The IDs of Custom Environments that the Environment Variable should be present on. At least one of `target` or `custom_environment_ids` must be set.", PlanModifiers: []planmodifier.Set{setplanmodifier.RequiresReplace(), setplanmodifier.UseStateForUnknown()}, Validators: []validator.Set{ setvalidator.SizeAtLeast(1), - setvalidator.ExactlyOneOf( + setvalidator.AtLeastOneOf( path.MatchRoot("custom_environment_ids"), path.MatchRoot("target"), ), diff --git a/vercel/resource_project_environment_variables.go b/vercel/resource_project_environment_variables.go index 58ec6a3e..fb1a7cd3 100644 --- a/vercel/resource_project_environment_variables.go +++ b/vercel/resource_project_environment_variables.go @@ -88,14 +88,12 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "The ID of the Environment Variable.", - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, - Computed: true, + Description: "The ID of the Environment Variable.", + Computed: true, }, "key": schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, - Description: "The name of the Environment Variable.", + Required: true, + Description: "The name of the Environment Variable.", }, "value": schema.StringAttribute{ Required: true, @@ -105,13 +103,13 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ "target": schema.SetAttribute{ Optional: true, Computed: true, - Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`.", PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}, + Description: "The environments that the Environment Variable should be present on. Valid targets are either `production`, `preview`, or `development`. At least one of `target` or `custom_environment_ids` must be set.", ElementType: types.StringType, Validators: []validator.Set{ setvalidator.ValueStringsAre(stringvalidator.OneOf("production", "preview", "development")), setvalidator.SizeAtLeast(1), - setvalidator.ExactlyOneOf( + setvalidator.AtLeastOneOf( path.MatchRelative().AtParent().AtName("custom_environment_ids"), path.MatchRelative().AtParent().AtName("target"), ), @@ -121,11 +119,11 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Optional: true, Computed: true, ElementType: types.StringType, - Description: "The IDs of Custom Environments that the Environment Variable should be present on. Cannot be set in conjuction with `target`.", PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}, + Description: "The IDs of Custom Environments that the Environment Variable should be present on. At least one of `target` or `custom_environment_ids` must be set.", Validators: []validator.Set{ setvalidator.SizeAtLeast(1), - setvalidator.ExactlyOneOf( + setvalidator.AtLeastOneOf( path.MatchRelative().AtParent().AtName("custom_environment_ids"), path.MatchRelative().AtParent().AtName("target"), ), @@ -139,13 +137,12 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ Description: "Whether the Environment Variable is sensitive or not.", Optional: true, Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace(), boolplanmodifier.UseStateForUnknown()}, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, }, "comment": schema.StringAttribute{ - Description: "A comment explaining what the environment variable is for.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + Description: "A comment explaining what the environment variable is for.", + Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.LengthBetween(0, 1000), }, @@ -296,13 +293,26 @@ func convertResponseToProjectEnvironmentVariables(ctx context.Context, response var env []attr.Value alreadyPresent := map[string]struct{}{} for _, e := range response { - target := []attr.Value{} - for _, t := range e.Target { - target = append(target, types.StringValue(t)) + var targetValue attr.Value + if len(e.Target) > 0 { + target := make([]attr.Value, 0, len(e.Target)) + for _, t := range e.Target { + target = append(target, types.StringValue(t)) + } + targetValue = types.SetValueMust(types.StringType, target) + } else { + targetValue = types.SetNull(types.StringType) } - var customEnvironmentIDs []attr.Value - for _, c := range e.CustomEnvironmentIDs { - customEnvironmentIDs = append(customEnvironmentIDs, types.StringValue(c)) + + var customEnvIDsValue attr.Value + if len(e.CustomEnvironmentIDs) > 0 { + customEnvIDs := make([]attr.Value, 0, len(e.CustomEnvironmentIDs)) + for _, c := range e.CustomEnvironmentIDs { + customEnvIDs = append(customEnvIDs, types.StringValue(c)) + } + customEnvIDsValue = types.SetValueMust(types.StringType, customEnvIDs) + } else { + customEnvIDsValue = types.SetNull(types.StringType) } value := types.StringValue(e.Value) if e.Type == "sensitive" { @@ -351,8 +361,8 @@ func convertResponseToProjectEnvironmentVariables(ctx context.Context, response map[string]attr.Value{ "key": types.StringValue(e.Key), "value": value, - "target": types.SetValueMust(types.StringType, target), - "custom_environment_ids": types.SetValueMust(types.StringType, customEnvironmentIDs), + "target": targetValue, + "custom_environment_ids": customEnvIDsValue, "git_branch": types.StringPointerValue(e.GitBranch), "id": types.StringValue(e.ID), "sensitive": types.BoolValue(e.Type == "sensitive"), @@ -509,20 +519,32 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re resp.Diagnostics.Append(diags...) return } - plannedIDs := map[string]struct{}{} + plannedEnvsByID := map[string]EnvironmentItem{} + toAdd := []EnvironmentItem{} for _, e := range planEnvs { - if e.ID.ValueString() == "" { - plannedIDs[e.ID.ValueString()] = struct{}{} + if e.ID.ValueString() != "" { + plannedEnvsByID[e.ID.ValueString()] = e + } else { + toAdd = append(toAdd, e) } } var toRemove []EnvironmentItem for _, e := range stateEnvs { - if _, ok := plannedIDs[e.ID.ValueString()]; !ok { + plannedEnv, ok := plannedEnvsByID[e.ID.ValueString()] + if !ok { toRemove = append(toRemove, e) + continue + } + if !plannedEnv.equal(&e) { + toRemove = append(toRemove, e) + toAdd = append(toAdd, plannedEnv) } } + tflog.Debug(ctx, "Removing environment variables", map[string]interface{}{"to_remove": toRemove}) + tflog.Debug(ctx, "Adding environment variables", map[string]interface{}{"to_add": toAdd}) + for _, v := range toRemove { err := r.client.DeleteEnvironmentVariable(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString(), v.ID.ValueString()) if err != nil { @@ -544,18 +566,26 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re }) } - request, diags := plan.toCreateEnvironmentVariablesRequest(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) - return - } - response, err := r.client.CreateEnvironmentVariables(ctx, request) - if err != nil { - resp.Diagnostics.AddError( - "Error updating project environment variables", - "Could not update project environment variable, unexpected error: "+err.Error(), - ) - return + var response []client.EnvironmentVariable + var err error + if len(toAdd) > 0 { + request, diags := plan.toCreateEnvironmentVariablesRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + tflog.Debug(ctx, "create request", map[string]any{ + "request": request, + }) + response, err = r.client.CreateEnvironmentVariables(ctx, request) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project environment variables", + "Could not update project environment variable, unexpected error: "+err.Error(), + ) + return + } + } else { } result, diags := convertResponseToProjectEnvironmentVariables(ctx, response, plan) diff --git a/vercel/resource_team_config.go b/vercel/resource_team_config.go index 979feca6..6625946d 100644 --- a/vercel/resource_team_config.go +++ b/vercel/resource_team_config.go @@ -131,6 +131,7 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques Validators: []validator.Map{ // Validate only this attribute or roles is configured. mapvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("saml.roles"), path.MatchRoot("saml.access_group_id"), }...), }, @@ -143,6 +144,7 @@ func (r *teamConfigResource) Schema(_ context.Context, req resource.SchemaReques // Validate only this attribute or roles is configured. stringvalidator.ExactlyOneOf(path.Expressions{ path.MatchRoot("saml.roles"), + path.MatchRoot("saml.access_group_id"), }...), }, }, From 76e6567cc7efcd9d1f19437db4d7e0de7abc2c50 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Thu, 19 Dec 2024 16:07:58 +0000 Subject: [PATCH 7/9] Fix project resource --- client/request.go | 3 ++ vercel/data_source_custom_environment_test.go | 4 +- vercel/resource_project.go | 48 +++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/client/request.go b/client/request.go index 7d54e00a..a366b6c9 100644 --- a/client/request.go +++ b/client/request.go @@ -126,6 +126,9 @@ func (c *Client) _doRequest(req *http.Request, v interface{}, errorOnNoContent b }{ Error: &errorResponse, }) + if errorResponse.Code == "" && errorResponse.Message == "" { + return fmt.Errorf("error performing API request: %d %s", resp.StatusCode, string(responseBody)) + } if err != nil { return fmt.Errorf("error unmarshaling response for status code %d: %w: %s", resp.StatusCode, err, string(responseBody)) } diff --git a/vercel/data_source_custom_environment_test.go b/vercel/data_source_custom_environment_test.go index 629de6fd..e51b0324 100644 --- a/vercel/data_source_custom_environment_test.go +++ b/vercel/data_source_custom_environment_test.go @@ -40,7 +40,7 @@ resource "vercel_project" "test" { resource "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc-custom-env-%[1]s" + name = "test-acc-ce-%[1]s" description = "oh cool" branch_tracking = { pattern = "staging-" @@ -51,7 +51,7 @@ resource "vercel_custom_environment" "test" { data "vercel_custom_environment" "test" { project_id = vercel_project.test.id %[2]s - name = "test-acc-custom-env-%[1]s" + name = vercel_custom_environment.test.name } `, projectSuffix, teamIDConfig()) } diff --git a/vercel/resource_project.go b/vercel/resource_project.go index f863ea38..944ae3c4 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -654,7 +654,7 @@ var nullProject = Project{ } func (p *Project) environment(ctx context.Context) ([]EnvironmentItem, error) { - if p.Environment.IsNull() { + if p.Environment.IsNull() || p.Environment.IsUnknown() { return nil, nil } @@ -669,12 +669,12 @@ func (p *Project) environment(ctx context.Context) ([]EnvironmentItem, error) { func parseEnvironment(ctx context.Context, vars []EnvironmentItem) (out []client.EnvironmentVariable, diags diag.Diagnostics) { for _, e := range vars { var target []string - diags = e.Target.ElementsAs(ctx, &target, false) + diags = e.Target.ElementsAs(ctx, &target, true) if diags.HasError() { return out, diags } var customEnvironmentIDs []string - diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, false) + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) if diags.HasError() { return out, diags } @@ -801,12 +801,12 @@ func (e *EnvironmentItem) equal(other *EnvironmentItem) bool { func (e *EnvironmentItem) toEnvironmentVariableRequest(ctx context.Context) (req client.EnvironmentVariableRequest, diags diag.Diagnostics) { var target []string - diags = e.Target.ElementsAs(ctx, &target, false) + diags = e.Target.ElementsAs(ctx, &target, true) if diags.HasError() { return req, diags } var customEnvironmentIDs []string - diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, false) + diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) if diags.HasError() { return req, diags } @@ -1246,13 +1246,26 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon var env []attr.Value for _, e := range environmentVariables { - target := []attr.Value{} - for _, t := range e.Target { - target = append(target, types.StringValue(t)) + var targetValue attr.Value + if len(e.Target) > 0 { + target := make([]attr.Value, 0, len(e.Target)) + for _, t := range e.Target { + target = append(target, types.StringValue(t)) + } + targetValue = types.SetValueMust(types.StringType, target) + } else { + targetValue = types.SetNull(types.StringType) } - var customEnvironmentIDs []attr.Value - for _, c := range e.CustomEnvironmentIDs { - customEnvironmentIDs = append(customEnvironmentIDs, types.StringValue(c)) + + var customEnvIDsValue attr.Value + if len(e.CustomEnvironmentIDs) > 0 { + customEnvIDs := make([]attr.Value, 0, len(e.CustomEnvironmentIDs)) + for _, c := range e.CustomEnvironmentIDs { + customEnvIDs = append(customEnvIDs, types.StringValue(c)) + } + customEnvIDsValue = types.SetValueMust(types.StringType, customEnvIDs) + } else { + customEnvIDsValue = types.SetNull(types.StringType) } value := types.StringValue(e.Value) if e.Type == "sensitive" { @@ -1263,11 +1276,16 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon } for _, p := range environment { var target []string - diags := p.Target.ElementsAs(ctx, &target, false) + diags := p.Target.ElementsAs(ctx, &target, true) + if diags.HasError() { + return Project{}, fmt.Errorf("error reading project environment variables: %s", diags) + } + var customEnvironmentIDs []string + diags = p.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true) if diags.HasError() { return Project{}, fmt.Errorf("error reading project environment variables: %s", diags) } - if p.Key.ValueString() == e.Key && isSameStringSet(target, e.Target) { + if p.Key.ValueString() == e.Key && isSameStringSet(target, e.Target) && isSameStringSet(customEnvironmentIDs, e.CustomEnvironmentIDs) { value = p.Value break } @@ -1277,8 +1295,8 @@ func convertResponseToProject(ctx context.Context, response client.ProjectRespon env = append(env, types.ObjectValueMust(envVariableElemType.AttrTypes, map[string]attr.Value{ "key": types.StringValue(e.Key), "value": value, - "target": types.SetValueMust(types.StringType, target), - "custom_environment_ids": types.SetValueMust(types.StringType, customEnvironmentIDs), + "target": targetValue, + "custom_environment_ids": customEnvIDsValue, "git_branch": types.StringPointerValue(e.GitBranch), "id": types.StringValue(e.ID), "sensitive": types.BoolValue(e.Type == "sensitive"), From 5bd793a85d93d451af4da122e162eb51dad81724 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Thu, 19 Dec 2024 16:20:18 +0000 Subject: [PATCH 8/9] Missed a schema change on the project data source --- vercel/data_source_project.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index c5ece7ee..3be3b0cf 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -104,6 +104,11 @@ For more detailed information, please see the [Vercel documentation](https://ver ElementType: types.StringType, Computed: true, }, + "custom_environment_ids": schema.SetAttribute{ + Description: "The IDs of Custom Environments that the Environment Variable should be present on.", + ElementType: types.StringType, + Computed: true, + }, "key": schema.StringAttribute{ Description: "The name of the environment variable.", Computed: true, From 2fd07f6c89722c6cb29c070619fecd65f6389422 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Thu, 19 Dec 2024 16:24:10 +0000 Subject: [PATCH 9/9] Generate docs... --- docs/data-sources/project.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index 78c3a4a2..cc53fed9 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -75,6 +75,7 @@ data "vercel_project" "example" { Read-Only: - `comment` (String) A comment explaining what the environment variable is for. +- `custom_environment_ids` (Set of String) The IDs of Custom Environments that the Environment Variable should be present on. - `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.