diff --git a/client/environment_variable_create.go b/client/environment_variable_create.go index d1d59d53..783dfa42 100644 --- a/client/environment_variable_create.go +++ b/client/environment_variable_create.go @@ -75,7 +75,7 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE return err } - tflog.Trace(ctx, "creating environment variable", map[string]interface{}{ + tflog.Trace(ctx, "creating environment variables", map[string]interface{}{ "url": url, "payload": payload, }) diff --git a/client/project_get.go b/client/project_get.go index 9bc43002..e4e03be6 100644 --- a/client/project_get.go +++ b/client/project_get.go @@ -55,6 +55,10 @@ func (r *ProjectResponse) Repository() *Repository { return nil } +type Protection struct { + DeploymentType string `json:"deploymentType"` +} + // ProjectResponse defines the information Vercel returns about a project. type ProjectResponse struct { BuildCommand *string `json:"buildCommand"` @@ -80,11 +84,13 @@ type ProjectResponse struct { // production branch ProductionBranch *string `json:"productionBranch"` } `json:"link"` - Name string `json:"name"` - OutputDirectory *string `json:"outputDirectory"` - PublicSource *bool `json:"publicSource"` - RootDirectory *string `json:"rootDirectory"` - ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` + Name string `json:"name"` + OutputDirectory *string `json:"outputDirectory"` + PublicSource *bool `json:"publicSource"` + RootDirectory *string `json:"rootDirectory"` + ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` + SSOProtection *Protection `json:"ssoProtection"` + PasswordProtection *Protection `json:"passwordProtection"` } // GetProject retrieves information about an existing project from Vercel. diff --git a/client/project_update.go b/client/project_update.go index 27e9bb12..08e8ef42 100644 --- a/client/project_update.go +++ b/client/project_update.go @@ -9,23 +9,30 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +type PasswordProtectionRequest struct { + DeploymentType string `json:"deploymentType"` + Password string `json:"password"` +} + // UpdateProjectRequest defines the possible fields that can be updated within a vercel project. // note that the values are all pointers, with many containing `omitempty` for serialisation. // This is because the Vercel API behaves in the following manner: // - a provided field will be updated -// - setting the field to an empty value (e.g. ”) will remove the setting for that field. +// - setting the field to an empty value (e.g. "") will remove the setting for that field. // - omitting the value entirely from the request will _not_ update the field. type UpdateProjectRequest struct { - BuildCommand *string `json:"buildCommand"` - CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` - DevCommand *string `json:"devCommand"` - Framework *string `json:"framework"` - InstallCommand *string `json:"installCommand"` - Name *string `json:"name,omitempty"` - OutputDirectory *string `json:"outputDirectory"` - PublicSource *bool `json:"publicSource"` - RootDirectory *string `json:"rootDirectory"` - ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` + BuildCommand *string `json:"buildCommand"` + CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` + DevCommand *string `json:"devCommand"` + Framework *string `json:"framework"` + InstallCommand *string `json:"installCommand"` + Name *string `json:"name,omitempty"` + OutputDirectory *string `json:"outputDirectory"` + PublicSource *bool `json:"publicSource"` + RootDirectory *string `json:"rootDirectory"` + ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` + SSOProtection *Protection `json:"ssoProtection"` + PasswordProtection *PasswordProtectionRequest `json:"passwordProtection"` } // UpdateProject updates an existing projects configuration within Vercel. diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index d60611f6..f2440ed4 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -38,6 +38,7 @@ output "project_id" { ### Optional +- `password_protection` (Attributes) Ensures visitors of your Preview Deployments must enter a password in order to gain access. (see [below for nested schema](#nestedatt--password_protection)) - `team_id` (String) The team ID the project exists beneath. ### Read-Only @@ -54,6 +55,15 @@ output "project_id" { - `public_source` (Boolean) Specifies whether the source code and logs of the deployments for this project should be public or not. - `root_directory` (String) The name of a directory or relative path to the source code of your project. When null is used it will default to the project root. - `serverless_function_region` (String) The region on Vercel's network to which your Serverless Functions are deployed. It should be close to any data source your Serverless Function might depend on. A new Deployment is required for your changes to take effect. Please see [Vercel's documentation](https://vercel.com/docs/concepts/edge-network/regions) for a full list of regions. +- `vercel_authentication` (Attributes) Ensures visitors to your Preview Deployments are logged into Vercel and have a minimum of Viewer access on your team. (see [below for nested schema](#nestedatt--vercel_authentication)) + + +### Nested Schema for `password_protection` + +Read-Only: + +- `protect_production` (Boolean) If true, production deployments will also be protected + ### Nested Schema for `environment` @@ -77,3 +87,11 @@ Read-Only: - `type` (String) The git provider of the repository. Must be either `github`, `gitlab`, or `bitbucket`. + +### Nested Schema for `vercel_authentication` + +Read-Only: + +- `protect_production` (Boolean) If true, production deployments will also be protected + + diff --git a/docs/resources/project.md b/docs/resources/project.md index 57cec7ce..6f2f60b4 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -63,10 +63,12 @@ resource "vercel_project" "example" { - `ignore_command` (String) When a commit is pushed to the Git repository that is connected with your Project, its SHA will determine if a new Build has to be issued. If the SHA was deployed before, no new Build will be issued. You can customize this behavior with a command that exits with code 1 (new Build needed) or code 0. - `install_command` (String) The install command for this project. If omitted, this value will be automatically detected. - `output_directory` (String) The output directory of the project. If omitted, this value will be automatically detected. +- `password_protection` (Attributes) Ensures visitors of your Preview Deployments must enter a password in order to gain access. (see [below for nested schema](#nestedatt--password_protection)) - `public_source` (Boolean) By default, visitors to the `/_logs` and `/_src` paths of your Production and Preview Deployments must log in with Vercel (requires being a member of your team) to see the Source, Logs and Deployment Status of your project. Setting `public_source` to `true` disables this behaviour, meaning the Source, Logs and Deployment Status can be publicly viewed. - `root_directory` (String) The name of a directory or relative path to the source code of your project. If omitted, it will default to the project root. - `serverless_function_region` (String) The region on Vercel's network to which your Serverless Functions are deployed. It should be close to any data source your Serverless Function might depend on. A new Deployment is required for your changes to take effect. Please see [Vercel's documentation](https://vercel.com/docs/concepts/edge-network/regions) for a full list of regions. - `team_id` (String) The team ID to add the project to. +- `vercel_authentication` (Attributes) Ensures visitors to your Preview Deployments are logged into Vercel and have a minimum of Viewer access on your team. (see [below for nested schema](#nestedatt--vercel_authentication)) ### Read-Only @@ -102,6 +104,26 @@ Optional: - `production_branch` (String) By default, every commit pushed to the main branch will trigger a Production Deployment instead of the usual Preview Deployment. You can switch to a different branch here. + + +### Nested Schema for `password_protection` + +Required: + +- `password` (String, Sensitive) The password that visitors must enter to gain access to your Preview Deployments. Drift detection is not possible for this field. + +Optional: + +- `protect_production` (Boolean) If true, production deployments will also be protected + + + +### Nested Schema for `vercel_authentication` + +Optional: + +- `protect_production` (Boolean) If true, production deployments will also be protected + ## Import Import is supported using the following syntax: diff --git a/vercel/data_source_alias.go b/vercel/data_source_alias.go index 59d9a0a9..3af2f7a0 100644 --- a/vercel/data_source_alias.go +++ b/vercel/data_source_alias.go @@ -10,6 +10,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &aliasDataSource{} +) + func newAliasDataSource() datasource.DataSource { return &aliasDataSource{} } diff --git a/vercel/data_source_file.go b/vercel/data_source_file.go index 05519206..bfa16d6e 100644 --- a/vercel/data_source_file.go +++ b/vercel/data_source_file.go @@ -13,6 +13,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &fileDataSource{} +) + func newFileDataSource() datasource.DataSource { return &fileDataSource{} } diff --git a/vercel/data_source_prebuilt_project.go b/vercel/data_source_prebuilt_project.go index 46643388..0edb5a98 100644 --- a/vercel/data_source_prebuilt_project.go +++ b/vercel/data_source_prebuilt_project.go @@ -16,6 +16,11 @@ import ( "github.com/vercel/terraform-provider-vercel/file" ) +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &prebuiltProjectDataSource{} +) + func newPrebuiltProjectDataSource() datasource.DataSource { return &prebuiltProjectDataSource{} } diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index 7272ae7d..6cf602fb 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -13,6 +13,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &projectDataSource{} +) + func newProjectDataSource() datasource.DataSource { return &projectDataSource{} } @@ -140,6 +145,26 @@ For more detailed information, please see the [Vercel documentation](https://ver }, }, }, + "vercel_authentication": schema.SingleNestedAttribute{ + Description: "Ensures visitors to your Preview Deployments are logged into Vercel and have a minimum of Viewer access on your team.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "protect_production": schema.BoolAttribute{ + Description: "If true, production deployments will also be protected", + Computed: true, + }, + }, + }, + "password_protection": schema.SingleNestedAttribute{ + Description: "Ensures visitors of your Preview Deployments must enter a password in order to gain access.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "protect_production": schema.BoolAttribute{ + Description: "If true, production deployments will also be protected", + Computed: true, + }, + }, + }, "id": schema.StringAttribute{ Computed: true, }, @@ -167,7 +192,7 @@ For more detailed information, please see the [Vercel documentation](https://ver // with this information. // It is called by the provider whenever data source values should be read to update state. func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config Project + var config ProjectDataSource diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -187,7 +212,7 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest return } - result := convertResponseToProject(out, config.coercedFields(), types.SetNull(envVariableElemType)) + result := convertResponseToProjectDataSource(out, nullProject) tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), diff --git a/vercel/data_source_project_directory.go b/vercel/data_source_project_directory.go index d173fbad..ac114ef3 100644 --- a/vercel/data_source_project_directory.go +++ b/vercel/data_source_project_directory.go @@ -14,6 +14,11 @@ import ( "github.com/vercel/terraform-provider-vercel/file" ) +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &projectDirectoryDataSource{} +) + func newProjectDirectoryDataSource() datasource.DataSource { return &projectDirectoryDataSource{} } diff --git a/vercel/data_source_project_model.go b/vercel/data_source_project_model.go new file mode 100644 index 00000000..451807cb --- /dev/null +++ b/vercel/data_source_project_model.go @@ -0,0 +1,59 @@ +package vercel + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/vercel/terraform-provider-vercel/client" +) + +// Project reflects the state terraform stores internally for a project. +type ProjectDataSource struct { + BuildCommand types.String `tfsdk:"build_command"` + DevCommand types.String `tfsdk:"dev_command"` + Environment types.Set `tfsdk:"environment"` + Framework types.String `tfsdk:"framework"` + GitRepository *GitRepository `tfsdk:"git_repository"` + ID types.String `tfsdk:"id"` + IgnoreCommand types.String `tfsdk:"ignore_command"` + InstallCommand types.String `tfsdk:"install_command"` + Name types.String `tfsdk:"name"` + OutputDirectory types.String `tfsdk:"output_directory"` + PublicSource types.Bool `tfsdk:"public_source"` + RootDirectory types.String `tfsdk:"root_directory"` + ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` + TeamID types.String `tfsdk:"team_id"` + VercelAuthentication *VercelAuthentication `tfsdk:"vercel_authentication"` + PasswordProtection *PasswordProtectionDataSource `tfsdk:"password_protection"` +} + +type PasswordProtectionDataSource struct { + ProtectProduction types.Bool `tfsdk:"protect_production"` +} + +func convertResponseToProjectDataSource(response client.ProjectResponse, plan Project) ProjectDataSource { + project := convertResponseToProject(response, plan) + + var pp *PasswordProtectionDataSource + if project.PasswordProtection != nil { + pp = &PasswordProtectionDataSource{ + ProtectProduction: project.PasswordProtection.ProtectProduction, + } + } + return ProjectDataSource{ + BuildCommand: project.BuildCommand, + DevCommand: project.DevCommand, + Environment: project.Environment, + Framework: project.Framework, + GitRepository: project.GitRepository, + ID: project.ID, + IgnoreCommand: project.IgnoreCommand, + InstallCommand: project.InstallCommand, + Name: project.Name, + OutputDirectory: project.OutputDirectory, + PublicSource: project.PublicSource, + RootDirectory: project.RootDirectory, + ServerlessFunctionRegion: project.ServerlessFunctionRegion, + TeamID: project.TeamID, + VercelAuthentication: project.VercelAuthentication, + PasswordProtection: pp, + } +} diff --git a/vercel/data_source_project_test.go b/vercel/data_source_project_test.go index afcadca7..b199f751 100644 --- a/vercel/data_source_project_test.go +++ b/vercel/data_source_project_test.go @@ -25,6 +25,8 @@ func TestAcc_ProjectDataSource(t *testing.T) { resource.TestCheckResourceAttr("data.vercel_project.test", "output_directory", ".output"), resource.TestCheckResourceAttr("data.vercel_project.test", "public_source", "true"), resource.TestCheckResourceAttr("data.vercel_project.test", "root_directory", "ui/src"), + resource.TestCheckResourceAttr("data.vercel_project.test", "vercel_authentication.protect_production", "true"), + resource.TestCheckResourceAttr("data.vercel_project.test", "password_protection.protect_production", "true"), resource.TestCheckTypeSetElemNestedAttrs("data.vercel_project.test", "environment.*", map[string]string{ "key": "foo", "value": "bar", @@ -47,6 +49,13 @@ resource "vercel_project" "test" { output_directory = ".output" public_source = true root_directory = "ui/src" + vercel_authentication = { + protect_production = true + } + password_protection = { + password = "foo" + protect_production = true + } %s environment = [ { diff --git a/vercel/resource_alias.go b/vercel/resource_alias.go index 58418263..c091ee24 100644 --- a/vercel/resource_alias.go +++ b/vercel/resource_alias.go @@ -12,6 +12,12 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &aliasResource{} + _ resource.ResourceWithConfigure = &aliasResource{} +) + func newAliasResource() resource.Resource { return &aliasResource{} } diff --git a/vercel/resource_deployment.go b/vercel/resource_deployment.go index bcd9abcb..d2d81f2d 100644 --- a/vercel/resource_deployment.go +++ b/vercel/resource_deployment.go @@ -23,6 +23,11 @@ import ( "github.com/vercel/terraform-provider-vercel/file" ) +var ( + _ resource.Resource = &deploymentResource{} + _ resource.ResourceWithConfigure = &deploymentResource{} +) + func newDeploymentResource() resource.Resource { return &deploymentResource{} } diff --git a/vercel/resource_dns_record.go b/vercel/resource_dns_record.go index 7a2903d1..52b9a5f1 100644 --- a/vercel/resource_dns_record.go +++ b/vercel/resource_dns_record.go @@ -14,6 +14,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +var ( + _ resource.Resource = &dnsRecordResource{} + _ resource.ResourceWithConfigure = &dnsRecordResource{} +) + func newDNSRecordResource() resource.Resource { return &dnsRecordResource{} } diff --git a/vercel/resource_project.go b/vercel/resource_project.go index f82ded56..c29b9314 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "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" @@ -16,6 +17,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +var ( + _ resource.Resource = &projectResource{} + _ resource.ResourceWithConfigure = &projectResource{} +) + func newProjectResource() resource.Resource { return &projectResource{} } @@ -162,6 +168,38 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ }, }, }, + "vercel_authentication": schema.SingleNestedAttribute{ + Description: "Ensures visitors to your Preview Deployments are logged into Vercel and have a minimum of Viewer access on your team.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "protect_production": schema.BoolAttribute{ + Description: "If true, production deployments will also be protected", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + "password_protection": schema.SingleNestedAttribute{ + Description: "Ensures visitors of your Preview Deployments must enter a password in order to gain access.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "password": schema.StringAttribute{ + Description: "The password that visitors must enter to gain access to your Preview Deployments. Drift detection is not possible for this field.", + Required: true, + Sensitive: true, + Validators: []validator.String{ + stringLengthBetween(1, 72), + }, + }, + "protect_production": schema.BoolAttribute{ + Description: "If true, production deployments will also be protected", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, "id": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, @@ -214,7 +252,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - result := convertResponseToProject(out, plan.coercedFields(), plan.Environment) + result := convertResponseToProject(out, plan) tflog.Trace(ctx, "created project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -225,6 +263,28 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } + if plan.PasswordProtection != nil || plan.VercelAuthentication != nil { + out, err = r.client.UpdateProject(ctx, result.ID.ValueString(), plan.TeamID.ValueString(), plan.toUpdateProjectRequest(plan.Name.ValueString()), !plan.Environment.IsNull()) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project as part of creating project", + "Could not update project, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToProject(out, plan) + tflog.Trace(ctx, "updated newly created project", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "project_id": result.ID.ValueString(), + }) + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + if plan.GitRepository == nil || plan.GitRepository.ProductionBranch.IsNull() || plan.GitRepository.ProductionBranch.IsUnknown() { return } @@ -242,7 +302,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - result = convertResponseToProject(out, plan.coercedFields(), plan.Environment) + result = convertResponseToProject(out, plan) tflog.Trace(ctx, "updated project production branch", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -282,7 +342,7 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } - result := convertResponseToProject(out, state.coercedFields(), state.Environment) + result := convertResponseToProject(out, state) tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -368,7 +428,7 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest return } - tflog.Error(ctx, "planEnvs", map[string]interface{}{ + tflog.Trace(ctx, "planEnvs", map[string]interface{}{ "plan_envs": planEnvs, "state_envs": stateEnvs, }) @@ -400,28 +460,30 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest items = append(items, v.toEnvironmentVariableRequest()) } - err = r.client.CreateEnvironmentVariables( - ctx, - client.CreateEnvironmentVariablesRequest{ - ProjectID: plan.ID.ValueString(), - TeamID: plan.TeamID.ValueString(), - EnvironmentVariables: items, - }, - ) - if err != nil { - resp.Diagnostics.AddError( - "Error updating project", - fmt.Sprintf( - "Could not upsert environment variables for project %s, unexpected error: %s", - plan.ID.ValueString(), - err, - ), + if items != nil { + err = r.client.CreateEnvironmentVariables( + ctx, + client.CreateEnvironmentVariablesRequest{ + ProjectID: plan.ID.ValueString(), + TeamID: plan.TeamID.ValueString(), + EnvironmentVariables: items, + }, ) + if err != nil { + resp.Diagnostics.AddError( + "Error updating project", + fmt.Sprintf( + "Could not upsert environment variables for project %s, unexpected error: %s", + plan.ID.ValueString(), + err, + ), + ) + } + tflog.Trace(ctx, "upserted environment variables", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "project_id": plan.ID.ValueString(), + }) } - tflog.Trace(ctx, "upserted environment variables", map[string]interface{}{ - "team_id": plan.TeamID.ValueString(), - "project_id": plan.ID.ValueString(), - }) out, err := r.client.UpdateProject(ctx, state.ID.ValueString(), state.TeamID.ValueString(), plan.toUpdateProjectRequest(state.Name.ValueString()), !plan.Environment.IsNull()) if err != nil { @@ -457,7 +519,7 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest } } - result := convertResponseToProject(out, plan.coercedFields(), plan.Environment) + result := convertResponseToProject(out, plan) tflog.Trace(ctx, "updated project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), @@ -540,14 +602,7 @@ func (r *projectResource) ImportState(ctx context.Context, req resource.ImportSt return } - result := convertResponseToProject(out, projectCoercedFields{ - /* As this is import, none of these fields are specified - so treat them all as Null */ - BuildCommand: types.StringNull(), - DevCommand: types.StringNull(), - InstallCommand: types.StringNull(), - OutputDirectory: types.StringNull(), - PublicSource: types.BoolNull(), - }, types.SetNull(envVariableElemType)) + result := convertResponseToProject(out, nullProject) tflog.Trace(ctx, "imported project", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "project_id": result.ID.ValueString(), diff --git a/vercel/resource_project_domain.go b/vercel/resource_project_domain.go index 45e1e635..53f910b3 100644 --- a/vercel/resource_project_domain.go +++ b/vercel/resource_project_domain.go @@ -14,6 +14,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +var ( + _ resource.Resource = &projectDomainResource{} + _ resource.ResourceWithConfigure = &projectDomainResource{} +) + func newProjectDomainResource() resource.Resource { return &projectDomainResource{} } diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index adeb9046..7f7da208 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -15,6 +15,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +var ( + _ resource.Resource = &projectEnvironmentVariableResource{} + _ resource.ResourceWithConfigure = &projectEnvironmentVariableResource{} +) + func newProjectEnvironmentVariableResource() resource.Resource { return &projectEnvironmentVariableResource{} } diff --git a/vercel/resource_project_model.go b/vercel/resource_project_model.go index 13738717..e0644dcc 100644 --- a/vercel/resource_project_model.go +++ b/vercel/resource_project_model.go @@ -11,20 +11,32 @@ import ( // Project reflects the state terraform stores internally for a project. type Project struct { - BuildCommand types.String `tfsdk:"build_command"` - DevCommand types.String `tfsdk:"dev_command"` - Environment types.Set `tfsdk:"environment"` - Framework types.String `tfsdk:"framework"` - GitRepository *GitRepository `tfsdk:"git_repository"` - ID types.String `tfsdk:"id"` - IgnoreCommand types.String `tfsdk:"ignore_command"` - InstallCommand types.String `tfsdk:"install_command"` - Name types.String `tfsdk:"name"` - OutputDirectory types.String `tfsdk:"output_directory"` - PublicSource types.Bool `tfsdk:"public_source"` - RootDirectory types.String `tfsdk:"root_directory"` - ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` - TeamID types.String `tfsdk:"team_id"` + BuildCommand types.String `tfsdk:"build_command"` + DevCommand types.String `tfsdk:"dev_command"` + Environment types.Set `tfsdk:"environment"` + Framework types.String `tfsdk:"framework"` + GitRepository *GitRepository `tfsdk:"git_repository"` + ID types.String `tfsdk:"id"` + IgnoreCommand types.String `tfsdk:"ignore_command"` + InstallCommand types.String `tfsdk:"install_command"` + Name types.String `tfsdk:"name"` + OutputDirectory types.String `tfsdk:"output_directory"` + PublicSource types.Bool `tfsdk:"public_source"` + RootDirectory types.String `tfsdk:"root_directory"` + ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` + TeamID types.String `tfsdk:"team_id"` + VercelAuthentication *VercelAuthentication `tfsdk:"vercel_authentication"` + PasswordProtection *PasswordProtection `tfsdk:"password_protection"` +} + +var nullProject = Project{ + /* As this is read only, none of these fields are specified - so treat them all as Null */ + BuildCommand: types.StringNull(), + DevCommand: types.StringNull(), + InstallCommand: types.StringNull(), + OutputDirectory: types.StringNull(), + PublicSource: types.BoolNull(), + Environment: types.SetNull(envVariableElemType), } func (p *Project) environment(ctx context.Context) ([]EnvironmentItem, error) { @@ -94,6 +106,8 @@ func (p *Project) toUpdateProjectRequest(oldName string) client.UpdateProjectReq PublicSource: toBoolPointer(p.PublicSource), RootDirectory: toStrPointer(p.RootDirectory), ServerlessFunctionRegion: toStrPointer(p.ServerlessFunctionRegion), + PasswordProtection: p.PasswordProtection.toUpdateProjectRequest(), + SSOProtection: p.VercelAuthentication.toUpdateProjectRequest(), } } @@ -137,6 +151,46 @@ func (g *GitRepository) toCreateProjectRequest() *client.GitRepository { } } +type VercelAuthentication struct { + ProtectProduction types.Bool `tfsdk:"protect_production"` +} + +func (v *VercelAuthentication) toUpdateProjectRequest() *client.Protection { + if v == nil { + return nil + } + + deploymentType := "preview" + if v.ProtectProduction.ValueBool() { + deploymentType = "all" + } + + return &client.Protection{ + DeploymentType: deploymentType, + } +} + +type PasswordProtection struct { + Password types.String `tfsdk:"password"` + ProtectProduction types.Bool `tfsdk:"protect_production"` +} + +func (p *PasswordProtection) toUpdateProjectRequest() *client.PasswordProtectionRequest { + if p == nil { + return nil + } + + deploymentType := "preview" + if p.ProtectProduction.ValueBool() { + deploymentType = "all" + } + + return &client.PasswordProtectionRequest{ + DeploymentType: deploymentType, + Password: p.Password.ValueString(), + } +} + /* * In the Vercel API the following fields are coerced to null during project creation @@ -195,7 +249,9 @@ var envVariableElemType = types.ObjectType{ }, } -func convertResponseToProject(response client.ProjectResponse, fields projectCoercedFields, environment types.Set) Project { +func convertResponseToProject(response client.ProjectResponse, plan Project) Project { + fields := plan.coercedFields() + var gr *GitRepository if repo := response.Repository(); repo != nil { gr = &GitRepository{ @@ -208,6 +264,25 @@ func convertResponseToProject(response client.ProjectResponse, fields projectCoe } } + var pp *PasswordProtection + if response.PasswordProtection != nil { + pass := types.StringValue("") + if plan.PasswordProtection != nil { + pass = plan.PasswordProtection.Password + } + pp = &PasswordProtection{ + Password: pass, + ProtectProduction: types.BoolValue(response.PasswordProtection.DeploymentType == "all"), + } + } + + var va *VercelAuthentication + if response.SSOProtection != nil { + va = &VercelAuthentication{ + ProtectProduction: types.BoolValue(response.SSOProtection.DeploymentType == "all"), + } + } + var env []attr.Value for _, e := range response.EnvironmentVariables { target := []attr.Value{} @@ -235,7 +310,7 @@ func convertResponseToProject(response client.ProjectResponse, fields projectCoe } environmentEntry := types.SetValueMust(envVariableElemType, env) - if len(response.EnvironmentVariables) == 0 && environment.IsNull() { + if len(response.EnvironmentVariables) == 0 && plan.Environment.IsNull() { environmentEntry = types.SetNull(envVariableElemType) } @@ -254,5 +329,7 @@ func convertResponseToProject(response client.ProjectResponse, fields projectCoe RootDirectory: fromStringPointer(response.RootDirectory), ServerlessFunctionRegion: fromStringPointer(response.ServerlessFunctionRegion), TeamID: toTeamID(response.TeamID), + PasswordProtection: pp, + VercelAuthentication: va, } } diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index 54a28f0f..e113a9e7 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -139,6 +139,48 @@ func TestAcc_ProjectWithGitRepository(t *testing.T) { }) } +func TestAcc_ProjectWithSSOAndPasswordProtection(t *testing.T) { + projectSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccProjectDestroy("vercel_project.enabled_to_start", testTeam()), + Steps: []resource.TestStep{ + { + Config: testAccProjectConfigWithSSOAndPassword(projectSuffix, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + testAccProjectExists("vercel_project.enabled_to_start", testTeam()), + resource.TestCheckResourceAttr("vercel_project.enabled_to_start", "vercel_authentication.protect_production", "true"), + resource.TestCheckResourceAttr("vercel_project.enabled_to_start", "password_protection.protect_production", "true"), + resource.TestCheckResourceAttr("vercel_project.enabled_to_start", "password_protection.password", "password"), + testAccProjectExists("vercel_project.disabled_to_start", testTeam()), + resource.TestCheckNoResourceAttr("vercel_project.disabled_to_start", "vercel_authentication"), + resource.TestCheckNoResourceAttr("vercel_project.disabled_to_start", "password_protection"), + testAccProjectExists("vercel_project.enabled_to_update", testTeam()), + resource.TestCheckResourceAttr("vercel_project.enabled_to_update", "vercel_authentication.protect_production", "false"), + resource.TestCheckResourceAttr("vercel_project.enabled_to_update", "password_protection.protect_production", "false"), + resource.TestCheckResourceAttr("vercel_project.enabled_to_update", "password_protection.password", "password"), + ), + }, + { + Config: testAccProjectConfigWithSSOAndPasswordUpdated(projectSuffix, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckNoResourceAttr("vercel_project.enabled_to_start", "vercel_authentication"), + resource.TestCheckNoResourceAttr("vercel_project.enabled_to_start", "password_protection"), + + resource.TestCheckResourceAttr("vercel_project.disabled_to_start", "vercel_authentication.protect_production", "true"), + resource.TestCheckResourceAttr("vercel_project.disabled_to_start", "password_protection.protect_production", "true"), + resource.TestCheckResourceAttr("vercel_project.disabled_to_start", "password_protection.password", "password"), + + resource.TestCheckResourceAttr("vercel_project.enabled_to_update", "vercel_authentication.protect_production", "true"), + resource.TestCheckResourceAttr("vercel_project.enabled_to_update", "password_protection.protect_production", "true"), + resource.TestCheckResourceAttr("vercel_project.enabled_to_update", "password_protection.password", "password2"), + ), + }, + }, + }) +} + func getProjectImportID(n string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[n] @@ -269,6 +311,72 @@ resource "vercel_project" "test" { `, projectSuffix, teamID) } +func testAccProjectConfigWithSSOAndPassword(projectSuffix, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "enabled_to_start" { + name = "test-acc-protection-one-%[1]s" + %[2]s + vercel_authentication = { + protect_production = true + } + password_protection = { + protect_production = true + password = "password" + } +} + +resource "vercel_project" "disabled_to_start" { + name = "test-acc-protection-two-%[1]s" + %[2]s +} + +resource "vercel_project" "enabled_to_update" { + name = "test-acc-protection-three-%[1]s" + %[2]s + vercel_authentication = { + protect_production = false + } + password_protection = { + protect_production = false + password = "password" + } +} + `, projectSuffix, teamID) +} + +func testAccProjectConfigWithSSOAndPasswordUpdated(projectSuffix, teamID string) string { + return fmt.Sprintf(` +resource "vercel_project" "enabled_to_start" { + name = "test-acc-protection-one-%[1]s" + %[2]s +} + +resource "vercel_project" "disabled_to_start" { + name = "test-acc-protection-two-%[1]s" + %[2]s + vercel_authentication = { + protect_production = true + } + password_protection = { + protect_production = true + password = "password" + } +} + +resource "vercel_project" "enabled_to_update" { + name = "test-acc-protection-three-%[1]s" + %[2]s + vercel_authentication = { + protect_production = true + } + password_protection = { + protect_production = true + password = "password2" + } +} + `, projectSuffix, teamID) +} + func testAccProjectConfigWithGitRepo(projectSuffix, teamID string) string { return fmt.Sprintf(` resource "vercel_project" "test_git" { diff --git a/vercel/resource_shared_environment_variable.go b/vercel/resource_shared_environment_variable.go index 9e5573f3..95bde157 100644 --- a/vercel/resource_shared_environment_variable.go +++ b/vercel/resource_shared_environment_variable.go @@ -15,6 +15,11 @@ import ( "github.com/vercel/terraform-provider-vercel/client" ) +var ( + _ resource.Resource = &sharedEnvironmentVariableResource{} + _ resource.ResourceWithConfigure = &sharedEnvironmentVariableResource{} +) + func newSharedEnvironmentVariableResource() resource.Resource { return &sharedEnvironmentVariableResource{} }