diff --git a/client/project.go b/client/project.go index 5b1cee76..98d7a87c 100644 --- a/client/project.go +++ b/client/project.go @@ -156,15 +156,23 @@ func (r *ProjectResponse) Repository() *Repository { return nil } +type ConnectConfigurationResponse struct { + Environment string `json:"envId"` + ConnectConfigurationID string `json:"connectConfigurationId"` + Passive bool `json:"passive"` + BuildsEnabled bool `json:"buildsEnabled"` +} + // ProjectResponse defines the information Vercel returns about a project. type ProjectResponse struct { - BuildCommand *string `json:"buildCommand"` - CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` - DevCommand *string `json:"devCommand"` - Framework *string `json:"framework"` - ID string `json:"id"` - TeamID string `json:"-"` - InstallCommand *string `json:"installCommand"` + BuildCommand *string `json:"buildCommand"` + CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` + ConnectConfigurations []ConnectConfigurationResponse `json:"connectConfigurations"` + DevCommand *string `json:"devCommand"` + Framework *string `json:"framework"` + ID string `json:"id"` + TeamID string `json:"-"` + InstallCommand *string `json:"installCommand"` Link *struct { Type string `json:"type"` // github @@ -346,6 +354,45 @@ func (c *Client) UpdateProject(ctx context.Context, projectID, teamID string, re return r, err } +// UpdateProjectSecureComputeNetworksRequest allows updating the Secure Compute Networks for a project. +type UpdateProjectSecureComputeNetworksRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + SecureComputeNetworks []ConnectConfigurationRequest `json:"connectConfigurations"` +} + +type ConnectConfigurationRequest struct { + Environment string `json:"envId"` + ConnectConfigurationID string `json:"connectConfigurationId"` + Passive bool `json:"passive"` + BuildsEnabled bool `json:"buildsEnabled"` +} + +// UpdateProjectSecureComputeNetworks updates an existing projects connectConfigurations within Vercel. +func (c *Client) UpdateProjectSecureComputeNetworks(ctx context.Context, request UpdateProjectSecureComputeNetworksRequest) (r ProjectResponse, err error) { + url := fmt.Sprintf("%s/v9/projects/%s", 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, "updating project", map[string]any{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + + r.TeamID = c.TeamID(request.TeamID) + return r, err +} + type UpdateProductionBranchRequest struct { TeamID string `json:"-"` ProjectID string `json:"-"` diff --git a/client/secure_compute_network.go b/client/secure_compute_network.go new file mode 100644 index 00000000..ce13c20f --- /dev/null +++ b/client/secure_compute_network.go @@ -0,0 +1,62 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// SecureComputeNetworkAWS represents the AWS configuration part of a Secure Compute Network. +type SecureComputeNetworkAWS struct { + AccountID string `json:"AccountId"` + Region string `json:"Region"` + ElasticIpAddresses []string `json:"ElasticIpAddresses,omitempty"` + LambdaRoleArn *string `json:"LambdaRoleArn,omitempty"` + SecurityGroupId *string `json:"SecurityGroupId,omitempty"` + StackId *string `json:"StackId,omitempty"` + SubnetIds []string `json:"SubnetIds,omitempty"` + SubscriptionArn *string `json:"SubscriptionArn,omitempty"` + VpcId *string `json:"VpcId,omitempty"` +} + +// SecureComputeNetwork represents a Vercel Secure Compute Network configuration. +type SecureComputeNetwork struct { + DC string `json:"dc"` + ProjectIDs []string `json:"projectIds,omitempty"` + ProjectsCount *int `json:"projectsCount,omitempty"` + PeeringConnectionsCount *int `json:"peeringConnectionsCount,omitempty"` + + AWS SecureComputeNetworkAWS `json:"AWS"` + ConfigurationName string `json:"ConfigurationName"` + ID string `json:"Id"` + TeamID string `json:"TeamId"` + CIDRBlock *string `json:"CidrBlock,omitempty"` + AvailabilityZoneIDs []string `json:"AvailabilityZoneIds,omitempty"` + Version string `json:"Version"` + ConfigurationStatus string `json:"ConfigurationStatus"` +} + +// ListSecureComputeNetworks fetches all Secure Compute Networks for a team. +func (c *Client) ListSecureComputeNetworks( + ctx context.Context, + teamID string, +) ([]SecureComputeNetwork, error) { + url := fmt.Sprintf("%s/v1/connect/configurations", c.baseURL) + if c.TeamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(teamID)) + } + tflog.Info(ctx, "reading secure compute networks", map[string]any{ + "url": url, + "team_id": c.TeamID(teamID), + }) + + var networks []SecureComputeNetwork + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &networks) + + return networks, err +} diff --git a/docs/data-sources/project_secure_compute_networks.md b/docs/data-sources/project_secure_compute_networks.md new file mode 100644 index 00000000..b330edfb --- /dev/null +++ b/docs/data-sources/project_secure_compute_networks.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_project_secure_compute_networks Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + +--- + +# vercel_project_secure_compute_networks (Data Source) + + + +## Example Usage + +```terraform +data "vercel_project" "example" { + name = "my-existing-project" +} + +data "vercel_project_secure_compute_networks" "example" { + project_id = data.vercel_project.example.id +} +``` + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project + +### Optional + +- `team_id` (String) The team ID. Required when configuring a team data source if a default team has not been set in the provider. + +### Read-Only + +- `secure_compute_networks` (Attributes Set) A set of Secure Compute Networks that the project should be configured with. (see [below for nested schema](#nestedatt--secure_compute_networks)) + + +### Nested Schema for `secure_compute_networks` + +Read-Only: + +- `builds_enabled` (Boolean) Whether the projects build container should be included in the Secure Compute Network. +- `environment` (String) The environment being configured. Should be one of 'production', 'preview', or the ID of a Custom Environment +- `network_id` (String) The ID of the Secure Compute Network to configure for this environment +- `passive` (Boolean) Whether the Secure Compute Network should be configured as a passive network, meaning it is used for passive failover. diff --git a/docs/data-sources/secure_compute_network.md b/docs/data-sources/secure_compute_network.md new file mode 100644 index 00000000..7be3eb7e --- /dev/null +++ b/docs/data-sources/secure_compute_network.md @@ -0,0 +1,55 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_secure_compute_network Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides information about an existing Vercel Secure Compute Network. + This data source allows you to retrieve details about a Secure Compute Network by its name and optional team ID. +--- + +# vercel_secure_compute_network (Data Source) + +Provides information about an existing Vercel Secure Compute Network. + +This data source allows you to retrieve details about a Secure Compute Network by its name and optional team ID. + + + + +## Schema + +### Required + +- `name` (String) The name of the Secure Compute Network configuration. + +### Optional + +- `team_id` (String) The ID of the Vercel team the Secure Compute Network belongs to. If omitted, the provider will use the team configured on the provider or the user's default team. + +### Read-Only + +- `availability_zone_ids` (Set of String) A set of AWS Availability Zone IDs where the Secure Compute Network resources are deployed. +- `aws` (Attributes) AWS configuration for the Secure Compute Network. (see [below for nested schema](#nestedatt--aws)) +- `cidr_block` (String) The CIDR block assigned to the Secure Compute Network. +- `configuration_status` (String) The operational status of the Secure Compute Network (e.g., 'ready', 'create_in_progress'). +- `dc` (String) The data center (region) associated with the Secure Compute Network. +- `id` (String) The unique identifier of the Secure Compute Network. +- `peering_connections_count` (Number) The number of peering connections established for this Secure Compute Network. +- `project_ids` (Set of String) A list of Vercel Project IDs connected to this Secure Compute Network. +- `projects_count` (Number) The number of Vercel Projects connected to this Secure Compute Network. +- `version` (String) The current version identifier of the Secure Compute Network configuration. + + +### Nested Schema for `aws` + +Read-Only: + +- `account_id` (String) The AWS account ID. +- `elastic_ip_addresses` (Set of String) A list of Elastic IP addresses. +- `lambda_role_arn` (String) The ARN of the Lambda role. +- `region` (String) The AWS region. +- `security_group_id` (String) The ID of the security group. +- `stack_id` (String) The ID of the CloudFormation stack. +- `subnet_ids` (Set of String) A list of subnet IDs. +- `subscription_arn` (String) The ARN of the subscription. +- `vpc_id` (String) The ID of the VPC. diff --git a/docs/resources/project_secure_compute_networks.md b/docs/resources/project_secure_compute_networks.md new file mode 100644 index 00000000..da6e7ee0 --- /dev/null +++ b/docs/resources/project_secure_compute_networks.md @@ -0,0 +1,72 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_project_secure_compute_networks Resource - terraform-provider-vercel" +subcategory: "" +description: |- + +--- + +# vercel_project_secure_compute_networks (Resource) + + + +## Example Usage + +```terraform +resource "vercel_project" "example" { + name = "example-project" +} + +data "vercel_secure_compute_network" "example" { + name = "Example Network" +} + +resource "vercel_project_secure_compute_networks" "example" { + project_id = vercel_project.example.id + secure_compute_networks = [ + { + environment = "production" + network_id = data.vercel_secure_compute_network.example.id + passive = false + builds_enabled = true + } + ] +} +``` + + +## Schema + +### Required + +- `project_id` (String) The ID of the Project +- `secure_compute_networks` (Attributes Set) A set of Secure Compute Networks that the project should be configured with. (see [below for nested schema](#nestedatt--secure_compute_networks)) + +### Optional + +- `team_id` (String) The team ID. Required when configuring a team resource if a default team has not been set in the provider. + + +### Nested Schema for `secure_compute_networks` + +Required: + +- `builds_enabled` (Boolean) Whether the projects build container should be included in the Secure Compute Network. +- `environment` (String) The environment being configured. Should be one of 'production', 'preview', or the ID of a Custom Environment +- `network_id` (String) The ID of the Secure Compute Network to configure for this environment +- `passive` (Boolean) Whether the Secure Compute Network should be configured as a passive network, meaning it is used for passive failover. + +## Import + +Import is supported using the following syntax: + +```shell +# If importing with a team configured on the provider, simply use the project ID. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_project_secure_compute_networks.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, you can import via the team_id and project_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. +terraform import vercel_project_secure_compute_networks.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` diff --git a/examples/data-sources/vercel_project_secure_compute_networks/data-source.tf b/examples/data-sources/vercel_project_secure_compute_networks/data-source.tf new file mode 100644 index 00000000..a4c2e768 --- /dev/null +++ b/examples/data-sources/vercel_project_secure_compute_networks/data-source.tf @@ -0,0 +1,7 @@ +data "vercel_project" "example" { + name = "my-existing-project" +} + +data "vercel_project_secure_compute_networks" "example" { + project_id = data.vercel_project.example.id +} diff --git a/examples/resources/vercel_project_secure_compute_networks/import.sh b/examples/resources/vercel_project_secure_compute_networks/import.sh new file mode 100644 index 00000000..854ab596 --- /dev/null +++ b/examples/resources/vercel_project_secure_compute_networks/import.sh @@ -0,0 +1,8 @@ +# If importing with a team configured on the provider, simply use the project ID. +# - project_id can be found in the project `settings` tab in the Vercel UI. +terraform import vercel_project_secure_compute_networks.example prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, you can import via the team_id and project_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. +terraform import vercel_project_secure_compute_networks.example team_xxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/examples/resources/vercel_project_secure_compute_networks/resource.tf b/examples/resources/vercel_project_secure_compute_networks/resource.tf new file mode 100644 index 00000000..fa65dfe0 --- /dev/null +++ b/examples/resources/vercel_project_secure_compute_networks/resource.tf @@ -0,0 +1,19 @@ +resource "vercel_project" "example" { + name = "example-project" +} + +data "vercel_secure_compute_network" "example" { + name = "Example Network" +} + +resource "vercel_project_secure_compute_networks" "example" { + project_id = vercel_project.example.id + secure_compute_networks = [ + { + environment = "production" + network_id = data.vercel_secure_compute_network.example.id + passive = false + builds_enabled = true + } + ] +} diff --git a/vercel/contains.go b/vercel/contains.go deleted file mode 100644 index 7441a49e..00000000 --- a/vercel/contains.go +++ /dev/null @@ -1,10 +0,0 @@ -package vercel - -func contains[T comparable](items []T, i T) bool { - for _, j := range items { - if j == i { - return true - } - } - return false -} diff --git a/vercel/convert.go b/vercel/convert.go new file mode 100644 index 00000000..2747efde --- /dev/null +++ b/vercel/convert.go @@ -0,0 +1,29 @@ +package vercel + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func intPtrToInt64Ptr(i *int) *int64 { + if i == nil { + return nil + } + val := int64(*i) + return &val +} + +func stringsToSet(ctx context.Context, strings []string) (types.Set, diag.Diagnostics) { + diags := diag.Diagnostics{} + stringSet := []attr.Value{} + for _, s := range strings { + stringSet = append(stringSet, types.StringValue(s)) + } + + set, d := types.SetValueFrom(ctx, types.StringType, stringSet) + diags.Append(d...) + return set, diags +} diff --git a/vercel/data_source_project_secure_compute_networks.go b/vercel/data_source_project_secure_compute_networks.go new file mode 100644 index 00000000..7bd1c5a4 --- /dev/null +++ b/vercel/data_source_project_secure_compute_networks.go @@ -0,0 +1,115 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +var ( + _ datasource.DataSource = &projectSecureComputeNetworksDataSource{} + _ datasource.DataSourceWithConfigure = &projectSecureComputeNetworksDataSource{} +) + +func newProjectSecureComputeNetworksDataSource() datasource.DataSource { + return &projectSecureComputeNetworksDataSource{} +} + +type projectSecureComputeNetworksDataSource struct { + client *client.Client +} + +func (r *projectSecureComputeNetworksDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_secure_compute_networks" +} + +func (r *projectSecureComputeNetworksDataSource) 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 DataSource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +// Schema returns the schema information for a microfrontendGroup data source. +func (r *projectSecureComputeNetworksDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` `, + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The ID of the Project", + Required: true, + }, + "team_id": schema.StringAttribute{ + Description: "The team ID. Required when configuring a team data source if a default team has not been set in the provider.", + Optional: true, + Computed: true, + }, + "secure_compute_networks": schema.SetNestedAttribute{ + Description: "A set of Secure Compute Networks that the project should be configured with.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "environment": schema.StringAttribute{ + Description: "The environment being configured. Should be one of 'production', 'preview', or the ID of a Custom Environment", + Computed: true, + }, + "network_id": schema.StringAttribute{ + Description: "The ID of the Secure Compute Network to configure for this environment", + Computed: true, + }, + "passive": schema.BoolAttribute{ + Description: "Whether the Secure Compute Network should be configured as a passive network, meaning it is used for passive failover.", + Computed: true, + }, + "builds_enabled": schema.BoolAttribute{ + Description: "Whether the projects build container should be included in the Secure Compute Network.", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (r *projectSecureComputeNetworksDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config ProjectSecureComputeNetworks + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetProject(ctx, config.ProjectID.ValueString(), config.TeamID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error reading projject secure compute networks", + fmt.Sprintf("Could not get project secure compute networks %s %s, unexpected error: %s", + config.TeamID.ValueString(), + config.ProjectID.ValueString(), + err, + ), + ) + return + } + + diags = resp.State.Set(ctx, convertResponseToProjectSecureComputeNetworks(out)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_project_secure_compute_networks_test.go b/vercel/data_source_project_secure_compute_networks_test.go new file mode 100644 index 00000000..3fd8ea59 --- /dev/null +++ b/vercel/data_source_project_secure_compute_networks_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_ProjectSecureComputeNetworksDataSource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg(fmt.Sprintf(` + resource "vercel_project" "test" { + name = "test-acc-project-%[1]s" + } + data "vercel_secure_compute_network" "test" { + name = "network 1" + } + data "vercel_secure_compute_network" "test_2" { + name = "network 2" + } + resource "vercel_project_secure_compute_networks" "test" { + project_id = vercel_project.test.id + secure_compute_networks = [ + { + environment = "production" + network_id = data.vercel_secure_compute_network.test.id + passive = true + builds_enabled = false + }, + { + environment = "preview" + network_id = data.vercel_secure_compute_network.test_2.id + passive = false + builds_enabled = false + } + ] + } + + data "vercel_project_secure_compute_networks" "test" { + project_id = vercel_project_secure_compute_networks.test.project_id + } + `, name)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.vercel_project_secure_compute_networks.test", "project_id"), + resource.TestCheckResourceAttrSet("data.vercel_project_secure_compute_networks.test", "team_id"), + resource.TestCheckResourceAttr("data.vercel_project_secure_compute_networks.test", "secure_compute_networks.#", "2"), + ), + }, + }, + }) +} diff --git a/vercel/data_source_secure_compute_network.go b/vercel/data_source_secure_compute_network.go new file mode 100644 index 00000000..5eb0eff6 --- /dev/null +++ b/vercel/data_source_secure_compute_network.go @@ -0,0 +1,312 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/v3/client" +) + +var ( + _ datasource.DataSource = &secureComputeNetworkDataSource{} + _ datasource.DataSourceWithConfigure = &secureComputeNetworkDataSource{} +) + +func newSecureComputeNetworkDataSource() datasource.DataSource { + return &secureComputeNetworkDataSource{} +} + +type secureComputeNetworkDataSource struct { + client *client.Client +} + +func (d *secureComputeNetworkDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secure_compute_network" +} + +func (d *secureComputeNetworkDataSource) 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 secureComputeNetwork data source +func (r *secureComputeNetworkDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides information about an existing Vercel Secure Compute Network. + +This data source allows you to retrieve details about a Secure Compute Network by its name and optional team ID. +`, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the Secure Compute Network configuration.", + Required: true, + }, + "team_id": schema.StringAttribute{ + Description: "The ID of the Vercel team the Secure Compute Network belongs to. " + + "If omitted, the provider will use the team configured on the provider or the user's default team.", + Optional: true, + }, + "id": schema.StringAttribute{ + Description: "The unique identifier of the Secure Compute Network.", + Computed: true, + }, + "dc": schema.StringAttribute{ + Description: "The data center (region) associated with the Secure Compute Network.", + Computed: true, + }, + "project_ids": schema.SetAttribute{ + Description: "A list of Vercel Project IDs connected to this Secure Compute Network.", + Computed: true, + ElementType: types.StringType, + }, + "projects_count": schema.Int64Attribute{ + Description: "The number of Vercel Projects connected to this Secure Compute Network.", + Computed: true, + }, + "peering_connections_count": schema.Int64Attribute{ + Description: "The number of peering connections established for this Secure Compute Network.", + Computed: true, + }, + "cidr_block": schema.StringAttribute{ + Description: "The CIDR block assigned to the Secure Compute Network.", + Computed: true, + }, + "availability_zone_ids": schema.SetAttribute{ + Description: "A set of AWS Availability Zone IDs where the Secure Compute Network resources are deployed.", + Computed: true, + ElementType: types.StringType, + }, + "version": schema.StringAttribute{ + Description: "The current version identifier of the Secure Compute Network configuration.", + Computed: true, + }, + "configuration_status": schema.StringAttribute{ + Description: "The operational status of the Secure Compute Network (e.g., 'ready', 'create_in_progress').", + Computed: true, + }, + "aws": schema.SingleNestedAttribute{ + Description: "AWS configuration for the Secure Compute Network.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "account_id": schema.StringAttribute{ + Description: "The AWS account ID.", + Computed: true, + }, + "region": schema.StringAttribute{ + Description: "The AWS region.", + Computed: true, + }, + "elastic_ip_addresses": schema.SetAttribute{ + Description: "A list of Elastic IP addresses.", + Computed: true, + ElementType: types.StringType, + }, + "lambda_role_arn": schema.StringAttribute{ + Description: "The ARN of the Lambda role.", + Computed: true, + }, + "security_group_id": schema.StringAttribute{ + Description: "The ID of the security group.", + Computed: true, + }, + "stack_id": schema.StringAttribute{ + Description: "The ID of the CloudFormation stack.", + Computed: true, + }, + "subnet_ids": schema.SetAttribute{ + Description: "A list of subnet IDs.", + Computed: true, + ElementType: types.StringType, + }, + "subscription_arn": schema.StringAttribute{ + Description: "The ARN of the subscription.", + Computed: true, + }, + "vpc_id": schema.StringAttribute{ + Description: "The ID of the VPC.", + Computed: true, + }, + }, + }, + }, + } +} + +type SecureComputeNetworkAWS struct { + AccountID types.String `tfsdk:"account_id"` + Region types.String `tfsdk:"region"` + ElasticIpAddresses types.Set `tfsdk:"elastic_ip_addresses"` + LambdaRoleArn types.String `tfsdk:"lambda_role_arn"` + SecurityGroupID types.String `tfsdk:"security_group_id"` + StackID types.String `tfsdk:"stack_id"` + SubnetIDs types.Set `tfsdk:"subnet_ids"` + SubscriptionArn types.String `tfsdk:"subscription_arn"` + VPCID types.String `tfsdk:"vpc_id"` +} + +var secureComputeNetworkAWSAttrTypes = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "account_id": types.StringType, + "region": types.StringType, + "elastic_ip_addresses": types.SetType{ + ElemType: types.StringType, + }, + "lambda_role_arn": types.StringType, + "security_group_id": types.StringType, + "stack_id": types.StringType, + "subnet_ids": types.SetType{ + ElemType: types.StringType, + }, + "subscription_arn": types.StringType, + "vpc_id": types.StringType, + }, +} + +type SecureComputeNetwork struct { + AvailabilityZoneIDs types.Set `tfsdk:"availability_zone_ids"` + CIDRBlock types.String `tfsdk:"cidr_block"` + ConfigurationStatus types.String `tfsdk:"configuration_status"` + DC types.String `tfsdk:"dc"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + PeeringConnectionsCount types.Int64 `tfsdk:"peering_connections_count"` + ProjectIDs types.Set `tfsdk:"project_ids"` + ProjectsCount types.Int64 `tfsdk:"projects_count"` + TeamID types.String `tfsdk:"team_id"` + Version types.String `tfsdk:"version"` + AWS types.Object `tfsdk:"aws"` +} + +func convertResponseToSecureComputeNetwork(ctx context.Context, response *client.SecureComputeNetwork) (out SecureComputeNetwork, diags diag.Diagnostics) { + projectIDs, ds := stringsToSet(ctx, response.ProjectIDs) + diags.Append(ds...) + if diags.HasError() { + return out, diags + } + + azIDs, ds := stringsToSet(ctx, response.AvailabilityZoneIDs) + diags.Append(ds...) + if diags.HasError() { + return SecureComputeNetwork{}, diags + } + + aws, ds := convertResponseToSecureComputeNetworkAWS(ctx, response.AWS) + diags.Append(ds...) + if diags.HasError() { + return SecureComputeNetwork{}, diags + } + + return SecureComputeNetwork{ + AvailabilityZoneIDs: azIDs, + CIDRBlock: types.StringPointerValue(response.CIDRBlock), + ConfigurationStatus: types.StringValue(response.ConfigurationStatus), + DC: types.StringValue(response.DC), + ID: types.StringValue(response.ID), + Name: types.StringValue(response.ConfigurationName), + PeeringConnectionsCount: types.Int64PointerValue(intPtrToInt64Ptr(response.PeeringConnectionsCount)), + ProjectIDs: projectIDs, + ProjectsCount: types.Int64PointerValue(intPtrToInt64Ptr(response.ProjectsCount)), + TeamID: types.StringValue(response.TeamID), + Version: types.StringValue(response.Version), + AWS: aws, + }, diags +} + +func convertResponseToSecureComputeNetworkAWS(ctx context.Context, aws client.SecureComputeNetworkAWS) (basetypes.ObjectValue, diag.Diagnostics) { + elasticIpAddresses, diags := stringsToSet(ctx, aws.ElasticIpAddresses) + if diags.HasError() { + return types.ObjectNull(secureComputeNetworkAWSAttrTypes.AttrTypes), diags + } + + subnetIDs, diags := stringsToSet(ctx, aws.SubnetIds) + if diags.HasError() { + return types.ObjectNull(secureComputeNetworkAWSAttrTypes.AttrTypes), diags + } + + return types.ObjectValueMust( + secureComputeNetworkAWSAttrTypes.AttrTypes, map[string]attr.Value{ + "account_id": types.StringValue(aws.AccountID), + "region": types.StringValue(aws.Region), + "elastic_ip_addresses": elasticIpAddresses, + "lambda_role_arn": types.StringPointerValue(aws.LambdaRoleArn), + "security_group_id": types.StringPointerValue(aws.SecurityGroupId), + "stack_id": types.StringPointerValue(aws.StackId), + "subnet_ids": subnetIDs, + "subscription_arn": types.StringPointerValue(aws.SubscriptionArn), + "vpc_id": types.StringPointerValue(aws.VpcId), + }), diags +} + +func (d *secureComputeNetworkDataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + var config SecureComputeNetwork + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Reading Secure Compute Network data source", map[string]any{"name": config.Name.ValueString(), "team_id": config.TeamID.ValueString()}) + + networks, err := d.client.ListSecureComputeNetworks(ctx, config.TeamID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Client Error", + fmt.Sprintf( + "Unable to list Secure Compute Networks: %s", + err, + ), + ) + return + } + + var network *client.SecureComputeNetwork + for i, n := range networks { + if n.ConfigurationName == config.Name.ValueString() { + network = &networks[i] + break + } + } + + if network == nil { + resp.Diagnostics.AddError("Secure Compute Network Not Found", fmt.Sprintf( + "No Secure Compute Network found with name '%s' for team_id '%s'", + config.Name.ValueString(), + d.client.TeamID(config.TeamID.ValueString()), + )) + return + } + + out, diags := convertResponseToSecureComputeNetwork(ctx, network) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, &out) + resp.Diagnostics.Append(diags...) +} diff --git a/vercel/data_source_secure_compute_network_test.go b/vercel/data_source_secure_compute_network_test.go new file mode 100644 index 00000000..c436c6d1 --- /dev/null +++ b/vercel/data_source_secure_compute_network_test.go @@ -0,0 +1,41 @@ +package vercel_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_SecureComputeNetworkDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg(` + data "vercel_secure_compute_network" "test" { + name = "network 1" + } + `), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "name", "network 1"), + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "team_id", testTeam(t)), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "id"), + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "dc", "sfo1"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "projects_count"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "peering_connections_count"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "cidr_block"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "version"), + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "configuration_status", "ready"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "aws.account_id"), + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "aws.region", "us-west-1"), + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "aws.elastic_ip_addresses.#", "2"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "aws.lambda_role_arn"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "aws.security_group_id"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "aws.stack_id"), + resource.TestCheckResourceAttr("data.vercel_secure_compute_network.test", "aws.subnet_ids.#", "2"), + resource.TestCheckResourceAttrSet("data.vercel_secure_compute_network.test", "aws.vpc_id"), + ), + }, + }, + }) +} diff --git a/vercel/data_source_shared_environment_variable.go b/vercel/data_source_shared_environment_variable.go index a0ab833d..45e410f9 100644 --- a/vercel/data_source_shared_environment_variable.go +++ b/vercel/data_source_shared_environment_variable.go @@ -3,6 +3,7 @@ package vercel import ( "context" "fmt" + "slices" "strings" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -148,7 +149,7 @@ func isSameTarget(a []string, b []types.String) bool { return false } for _, v := range b { - if !contains(a, v.ValueString()) { + if !slices.Contains(a, v.ValueString()) { return false } } diff --git a/vercel/provider.go b/vercel/provider.go index 2def273a..dbb6febc 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -56,29 +56,30 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newAliasResource, newAttackChallengeModeResource, newCustomEnvironmentResource, - newDNSRecordResource, newDeploymentResource, + newDNSRecordResource, newEdgeConfigItemResource, newEdgeConfigResource, newEdgeConfigSchemaResource, newEdgeConfigTokenResource, - newFirewallConfigResource, newFirewallBypassResource, + newFirewallConfigResource, newIntegrationProjectAccessResource, newLogDrainResource, + newMicrofrontendGroupMembershipResource, + newMicrofrontendGroupResource, newProjectDeploymentRetentionResource, newProjectDomainResource, newProjectEnvironmentVariableResource, newProjectEnvironmentVariablesResource, newProjectMembersResource, newProjectResource, - newSharedEnvironmentVariableResource, + newProjectSecureComputeNetworksResource, newSharedEnvironmentVariableProjectLinkResource, + newSharedEnvironmentVariableResource, newTeamConfigResource, newTeamMemberResource, newWebhookResource, - newMicrofrontendGroupResource, - newMicrofrontendGroupMembershipResource, } } @@ -97,16 +98,18 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newEndpointVerificationDataSource, newFileDataSource, newLogDrainDataSource, + newMicrofrontendGroupDataSource, + newMicrofrontendGroupMembershipDataSource, newPrebuiltProjectDataSource, newProjectDataSource, newProjectDeploymentRetentionDataSource, newProjectDirectoryDataSource, newProjectMembersDataSource, + newProjectSecureComputeNetworksDataSource, + newSecureComputeNetworkDataSource, newSharedEnvironmentVariableDataSource, newTeamConfigDataSource, newTeamMemberDataSource, - newMicrofrontendGroupDataSource, - newMicrofrontendGroupMembershipDataSource, } } diff --git a/vercel/resource_project.go b/vercel/resource_project.go index bbd55e52..365d822f 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "slices" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" @@ -1196,7 +1197,7 @@ func isSameStringSet(a []string, b []string) bool { return false } for _, v := range a { - if !contains(b, v) { + if !slices.Contains(b, v) { return false } } diff --git a/vercel/resource_project_secure_compute_networks.go b/vercel/resource_project_secure_compute_networks.go new file mode 100644 index 00000000..f4b33652 --- /dev/null +++ b/vercel/resource_project_secure_compute_networks.go @@ -0,0 +1,332 @@ +package vercel + +import ( + "context" + "fmt" + + "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/planmodifier" + "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-log/tflog" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +var ( + _ resource.Resource = &projectSecureComputeNetworksResource{} + _ resource.ResourceWithConfigure = &projectSecureComputeNetworksResource{} + _ resource.ResourceWithImportState = &projectSecureComputeNetworksResource{} +) + +func newProjectSecureComputeNetworksResource() resource.Resource { + return &projectSecureComputeNetworksResource{} +} + +type projectSecureComputeNetworksResource struct { + client *client.Client +} + +func (r *projectSecureComputeNetworksResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_secure_compute_networks" +} + +func (r *projectSecureComputeNetworksResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.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 Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +// Schema returns the schema information for a microfrontendGroup resource. +func (r *projectSecureComputeNetworksResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` `, + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The ID of the Project", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "team_id": schema.StringAttribute{ + Description: "The team ID. Required when configuring a team resource if a default team has not been set in the provider.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + }, + "secure_compute_networks": schema.SetNestedAttribute{ + Description: "A set of Secure Compute Networks that the project should be configured with.", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "environment": schema.StringAttribute{ + Description: "The environment being configured. Should be one of 'production', 'preview', or the ID of a Custom Environment", + Required: true, + }, + "network_id": schema.StringAttribute{ + Description: "The ID of the Secure Compute Network to configure for this environment", + Required: true, + }, + "passive": schema.BoolAttribute{ + Description: "Whether the Secure Compute Network should be configured as a passive network, meaning it is used for passive failover.", + Required: true, + }, + "builds_enabled": schema.BoolAttribute{ + Description: "Whether the projects build container should be included in the Secure Compute Network.", + Required: true, + }, + }, + }, + Validators: []validator.Set{ + NewPassiveBuildsEnabledValidator(), + }, + }, + }, + } +} + +type ProjectSecureComputeNetwork struct { + Environment types.String `tfsdk:"environment"` + NetworkID types.String `tfsdk:"network_id"` + Passive types.Bool `tfsdk:"passive"` + BuildsEnabled types.Bool `tfsdk:"builds_enabled"` +} + +var projectSecureComputeNetworkElemType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "environment": types.StringType, + "network_id": types.StringType, + "passive": types.BoolType, + "builds_enabled": types.BoolType, + }, +} + +type ProjectSecureComputeNetworks struct { + TeamID types.String `tfsdk:"team_id"` + ProjectID types.String `tfsdk:"project_id"` + SecureComputeNetworks types.Set `tfsdk:"secure_compute_networks"` +} + +func (p ProjectSecureComputeNetworks) toUpdateProjectSecureComputeNetworksRequest(ctx context.Context) (client.UpdateProjectSecureComputeNetworksRequest, diag.Diagnostics) { + var networks []ProjectSecureComputeNetwork + diags := p.SecureComputeNetworks.ElementsAs(ctx, &networks, false) + scNetworks := make([]client.ConnectConfigurationRequest, 0, len(networks)) + for _, n := range networks { + scNetworks = append(scNetworks, client.ConnectConfigurationRequest{ + Environment: n.Environment.ValueString(), + ConnectConfigurationID: n.NetworkID.ValueString(), + Passive: n.Passive.ValueBool(), + BuildsEnabled: n.BuildsEnabled.ValueBool(), + }) + } + return client.UpdateProjectSecureComputeNetworksRequest{ + TeamID: p.TeamID.ValueString(), + ProjectID: p.ProjectID.ValueString(), + SecureComputeNetworks: scNetworks, + }, diags +} + +func convertResponseToProjectSecureComputeNetworks(response client.ProjectResponse) ProjectSecureComputeNetworks { + networks := make([]attr.Value, 0, len(response.ConnectConfigurations)) + for _, n := range response.ConnectConfigurations { + networks = append(networks, types.ObjectValueMust(projectSecureComputeNetworkElemType.AttrTypes, map[string]attr.Value{ + "environment": types.StringValue(n.Environment), + "network_id": types.StringValue(n.ConnectConfigurationID), + "passive": types.BoolValue(n.Passive), + "builds_enabled": types.BoolValue(n.BuildsEnabled), + })) + } + + return ProjectSecureComputeNetworks{ + TeamID: types.StringValue(response.TeamID), + ProjectID: types.StringValue(response.ID), + SecureComputeNetworks: types.SetValueMust(projectSecureComputeNetworkElemType, networks), + } +} + +func (r *projectSecureComputeNetworksResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ProjectSecureComputeNetworks + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting project secure compute networks plan", + "Error getting project secure compute networks plan", + ) + return + } + + tflog.Info(ctx, "creating project secure compute networks", map[string]any{ + "team_id": plan.TeamID.ValueString(), + "project_id": plan.ProjectID.ValueString(), + }) + + request, diags := plan.toUpdateProjectSecureComputeNetworksRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + out, err := r.client.UpdateProjectSecureComputeNetworks(ctx, request) + if err != nil { + resp.Diagnostics.AddError( + "Error creating project secure compute networks", + "Could not create project secure compute networks, unexpected error: "+err.Error(), + ) + return + } + + tflog.Info(ctx, "created secure compute networks", map[string]any{ + "team_id": plan.TeamID.ValueString(), + "project_id": plan.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, convertResponseToProjectSecureComputeNetworks(out)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *projectSecureComputeNetworksResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ProjectSecureComputeNetworks + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetProject(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading projject secure compute networks", + fmt.Sprintf("Could not get project secure compute networks %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + diags = resp.State.Set(ctx, convertResponseToProjectSecureComputeNetworks(out)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *projectSecureComputeNetworksResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ProjectSecureComputeNetworks + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting project secure compute networks plan", + "Error getting project secure compute networks plan", + ) + return + } + + request, diags := plan.toUpdateProjectSecureComputeNetworksRequest(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + out, err := r.client.UpdateProjectSecureComputeNetworks(ctx, request) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project secure compute networks", + "Could not update project secure compute networks, unexpected error: "+err.Error(), + ) + return + } + + tflog.Info(ctx, "created secure compute networks", map[string]any{ + "team_id": plan.TeamID.ValueString(), + "project_id": plan.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, convertResponseToProjectSecureComputeNetworks(out)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *projectSecureComputeNetworksResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ProjectSecureComputeNetworks + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "deleting secure compute networks", map[string]any{ + "project_id": state.ProjectID.ValueString(), + "team_id": state.TeamID.ValueString(), + }) + + _, err := r.client.UpdateProjectSecureComputeNetworks(ctx, client.UpdateProjectSecureComputeNetworksRequest{ + TeamID: state.TeamID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + SecureComputeNetworks: nil, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting project secure compute networks", + "Could not delete project secure compute networks, unexpected error: "+err.Error(), + ) + return + } +} + +func (r *projectSecureComputeNetworksResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, projectID, ok := splitInto1Or2(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Project Secure Compute Networks", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/project_id\" or \"project_id\"", req.ID), + ) + } + out, err := r.client.GetProject(ctx, projectID, teamID) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading project secure compute networks", + fmt.Sprintf("Could not get project secure compute networks %s %s, unexpected error: %s", + teamID, + projectID, + err, + ), + ) + return + } + + diags := resp.State.Set(ctx, convertResponseToProjectSecureComputeNetworks(out)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/resource_project_secure_compute_networks_test.go b/vercel/resource_project_secure_compute_networks_test.go new file mode 100644 index 00000000..5a83ebd2 --- /dev/null +++ b/vercel/resource_project_secure_compute_networks_test.go @@ -0,0 +1,149 @@ +package vercel_test + +import ( + "context" + "fmt" + "regexp" + "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/v3/client" +) + +func testCheckProjectSecureComputeNetworksDeleted(testClient *client.Client, n, teamID string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no project ID is set") + } + + project, err := testClient.GetProject(context.TODO(), rs.Primary.ID, teamID) + if err != nil { + return fmt.Errorf("unexpected error %w", err) + } + + if len(project.ConnectConfigurations) > 0 { + return fmt.Errorf("expected no connect configurations, but got %d", len(project.ConnectConfigurations)) + } + + return nil + } +} + +func TestAcc_ProjectSecureComputeNetworksResource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg(fmt.Sprintf(` + resource "vercel_project" "test" { + name = "test-acc-project-%[1]s" + } + data "vercel_secure_compute_network" "test" { + name = "network 1" + } + resource "vercel_project_secure_compute_networks" "test" { + project_id = vercel_project.test.id + secure_compute_networks = [ + { + environment = "production" + network_id = data.vercel_secure_compute_network.test.id + passive = true + builds_enabled = true + } + ] + } + `, name)), + ExpectError: regexp.MustCompile("builds_enabled cannot be `true` if passive is `true`"), + }, + { + Config: cfg(fmt.Sprintf(` + resource "vercel_project" "test" { + name = "test-acc-project-%[1]s" + } + data "vercel_secure_compute_network" "test" { + name = "network 1" + } + resource "vercel_project_secure_compute_networks" "test" { + project_id = vercel_project.test.id + secure_compute_networks = [ + { + environment = "production" + network_id = data.vercel_secure_compute_network.test.id + passive = false + builds_enabled = true + } + ] + } + `, name)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "project_id"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.#", "1"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.environment", "production"), + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.network_id"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.passive", "false"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.builds_enabled", "true"), + ), + }, + { + Config: cfg(fmt.Sprintf(` + resource "vercel_project" "test" { + name = "test-acc-project-%[1]s" + } + data "vercel_secure_compute_network" "test" { + name = "network 1" + } + data "vercel_secure_compute_network" "test_2" { + name = "network 2" + } + resource "vercel_project_secure_compute_networks" "test" { + project_id = vercel_project.test.id + secure_compute_networks = [ + { + environment = "preview" + network_id = data.vercel_secure_compute_network.test.id + passive = true + builds_enabled = false + }, + { + environment = "production" + network_id = data.vercel_secure_compute_network.test_2.id + passive = true + builds_enabled = false + }, + ] + } + `, name)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "project_id"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.#", "2"), + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.environment"), + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.network_id"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.passive", "true"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.0.builds_enabled", "false"), + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "secure_compute_networks.1.environment"), + resource.TestCheckResourceAttrSet("vercel_project_secure_compute_networks.test", "secure_compute_networks.1.network_id"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.1.passive", "true"), + resource.TestCheckResourceAttr("vercel_project_secure_compute_networks.test", "secure_compute_networks.1.builds_enabled", "false"), + ), + }, + { + Config: cfg(fmt.Sprintf(` + resource "vercel_project" "test" { + name = "test-acc-project-%[1]s" + } + `, name)), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckProjectSecureComputeNetworksDeleted(testClient(t), "vercel_project.test", testTeam(t)), + ), + }, + }, + }) +} diff --git a/vercel/resource_team_member.go b/vercel/resource_team_member.go index 9cc51934..710f13cd 100644 --- a/vercel/resource_team_member.go +++ b/vercel/resource_team_member.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "slices" "time" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -227,12 +228,12 @@ func areSameProjects(oldProjects, newProjects []TeamMemberProject) bool { return false } for _, p := range oldProjects { - if !contains(newProjects, p) { + if !slices.Contains(newProjects, p) { return false } } for _, p := range newProjects { - if !contains(oldProjects, p) { + if !slices.Contains(oldProjects, p) { return false } } @@ -241,12 +242,12 @@ func areSameProjects(oldProjects, newProjects []TeamMemberProject) bool { func diffAccessGroups(oldAgs, newAgs []string) (toAdd, toRemove []string) { for _, n := range newAgs { - if !contains(oldAgs, n) { + if !slices.Contains(oldAgs, n) { toAdd = append(toAdd, n) } } for _, n := range oldAgs { - if !contains(newAgs, n) { + if !slices.Contains(newAgs, n) { toRemove = append(toRemove, n) } } diff --git a/vercel/validator_passive_builds_enabled.go b/vercel/validator_passive_builds_enabled.go new file mode 100644 index 00000000..a6dfd86b --- /dev/null +++ b/vercel/validator_passive_builds_enabled.go @@ -0,0 +1,42 @@ +package vercel + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// passiveBuildsEnabledValidator is a validator that ensures builds_enabled is false if passive is true. +type passiveBuildsEnabledValidator struct{} + +func (v passiveBuildsEnabledValidator) Description(_ context.Context) string { + return "builds_enabled cannot be true if passive is true" +} + +func (v passiveBuildsEnabledValidator) MarkdownDescription(_ context.Context) string { + return "builds_enabled cannot be true if passive is true" +} + +func (v passiveBuildsEnabledValidator) ValidateSet(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + // Iterate through each element in the set + var networks []ProjectSecureComputeNetwork + diags := req.ConfigValue.ElementsAs(ctx, &networks, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + for _, network := range networks { + // Check the condition + if network.Passive.ValueBool() && network.BuildsEnabled.ValueBool() { + resp.Diagnostics.AddError( + "Invalid Secure Compute Network Configuration", + "builds_enabled cannot be `true` if passive is `true`.", + ) + } + } +} + +// NewPassiveBuildsEnabledValidator returns a validator that ensures builds_enabled is false if passive is true. +func NewPassiveBuildsEnabledValidator() validator.Set { + return passiveBuildsEnabledValidator{} +} diff --git a/vercel/validator_serverless_function_region.go b/vercel/validator_serverless_function_region.go index 43d42a1b..2fe3d810 100644 --- a/vercel/validator_serverless_function_region.go +++ b/vercel/validator_serverless_function_region.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "slices" "strings" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -87,7 +88,7 @@ func (v validatorServerlessFunctionRegion) ValidateString(ctx context.Context, r } for region, regionInfo := range regions { - if contains(regionInfo.Caps, "V2_DEPLOYMENT_CREATE") { + if slices.Contains(regionInfo.Caps, "V2_DEPLOYMENT_CREATE") { if v.regions == nil { v.regions = map[string]struct{}{} }