diff --git a/docs/resources/project.md b/docs/resources/project.md index ad9fcc2c..a87e2308 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -74,7 +74,7 @@ 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. -- `public_source` (Boolean) Specifies whether the source code and logs of the deployments for this project should be public or not. +- `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. diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index b32739fb..2c8a1f1a 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -183,7 +183,7 @@ func (r dataSourceProject) Read(ctx context.Context, req datasource.ReadRequest, return } - result := convertResponseToProject(out, config.TeamID) + result := convertResponseToProject(out, config.coercedFields()) tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, diff --git a/vercel/resource_project.go b/vercel/resource_project.go index b92d7e71..126c21fb 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -174,7 +174,7 @@ For more detailed information, please see the [Vercel documentation](https://ver "public_source": { Optional: true, Type: types.BoolType, - Description: "Specifies whether the source code and logs of the deployments for this project should be public or not.", + Description: "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": { Optional: true, @@ -223,7 +223,7 @@ func (r resourceProject) Create(ctx context.Context, req resource.CreateRequest, return } - result := convertResponseToProject(out, plan.TeamID) + result := convertResponseToProject(out, plan.coercedFields()) tflog.Trace(ctx, "created project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, @@ -263,7 +263,7 @@ func (r resourceProject) Read(ctx context.Context, req resource.ReadRequest, res return } - result := convertResponseToProject(out, state.TeamID) + result := convertResponseToProject(out, state.coercedFields()) tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, @@ -392,7 +392,7 @@ func (r resourceProject) Update(ctx context.Context, req resource.UpdateRequest, return } - result := convertResponseToProject(out, plan.TeamID) + result := convertResponseToProject(out, plan.coercedFields()) tflog.Trace(ctx, "updated project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, @@ -476,11 +476,15 @@ func (r resourceProject) ImportState(ctx context.Context, req resource.ImportSta return } - stringTypeTeamID := types.String{Value: teamID} - if teamID == "" { - stringTypeTeamID.Null = true - } - result := convertResponseToProject(out, stringTypeTeamID) + result := convertResponseToProject(out, projectCoercedFields{ + /* As this is import, none of these fields are specified - so treat them all as Null */ + BuildCommand: types.String{Null: true}, + DevCommand: types.String{Null: true}, + InstallCommand: types.String{Null: true}, + OutputDirectory: types.String{Null: true}, + PublicSource: types.Bool{Null: true}, + TeamID: types.String{Value: teamID, Null: teamID == ""}, + }) tflog.Trace(ctx, "imported project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, diff --git a/vercel/resource_project_model.go b/vercel/resource_project_model.go index f135a190..319beb60 100644 --- a/vercel/resource_project_model.go +++ b/vercel/resource_project_model.go @@ -119,7 +119,55 @@ func (g *GitRepository) toCreateProjectRequest() *client.GitRepository { } } -func convertResponseToProject(response client.ProjectResponse, tid types.String) Project { +/* +* In the Vercel API the following fields are coerced to null during project creation + +* This causes an issue when they are specified, but falsy, as the +* terraform configuration explicitly sets a value for them, but the Vercel +* API returns a different value. This causes an inconsistent plan error. + +* We avoid this issue by choosing to use values from the terraform state, +* but only if they are _explicitly stated_ *and* they are _falsy_ values +* *and* the response value was null. This is important as drift detection +* would fail to work if the value was always selected, so this is as stringent +* as possible to allow drift-detection in the majority of scenarios. + +* This is implemented in the below uncoerceString and uncoerceBool functions. + */ +type projectCoercedFields struct { + BuildCommand types.String + DevCommand types.String + InstallCommand types.String + OutputDirectory types.String + PublicSource types.Bool + TeamID types.String +} + +func (p *Project) coercedFields() projectCoercedFields { + return projectCoercedFields{ + BuildCommand: p.BuildCommand, + DevCommand: p.DevCommand, + InstallCommand: p.InstallCommand, + OutputDirectory: p.OutputDirectory, + PublicSource: p.PublicSource, + TeamID: p.TeamID, + } +} + +func uncoerceString(plan, res types.String) types.String { + if plan.Value == "" && !plan.Null && res.Null { + return plan + } + return res +} +func uncoerceBool(plan, res types.Bool) types.Bool { + if !plan.Value && !plan.Null && res.Null { + return plan + } + return res +} + +func convertResponseToProject(response client.ProjectResponse, fields projectCoercedFields) Project { var gr *GitRepository if repo := response.Repository(); repo != nil { gr = &GitRepository{ @@ -141,25 +189,21 @@ func convertResponseToProject(response client.ProjectResponse, tid types.String) ID: types.String{Value: e.ID}, }) } - teamID := types.String{Value: tid.Value} - if tid.Unknown || tid.Null { - teamID.Null = true - } return Project{ - BuildCommand: fromStringPointer(response.BuildCommand), - DevCommand: fromStringPointer(response.DevCommand), + BuildCommand: uncoerceString(fields.BuildCommand, fromStringPointer(response.BuildCommand)), + DevCommand: uncoerceString(fields.DevCommand, fromStringPointer(response.DevCommand)), Environment: env, Framework: fromStringPointer(response.Framework), GitRepository: gr, ID: types.String{Value: response.ID}, IgnoreCommand: fromStringPointer(response.CommandForIgnoringBuildStep), - InstallCommand: fromStringPointer(response.InstallCommand), + InstallCommand: uncoerceString(fields.InstallCommand, fromStringPointer(response.InstallCommand)), Name: types.String{Value: response.Name}, - OutputDirectory: fromStringPointer(response.OutputDirectory), - PublicSource: fromBoolPointer(response.PublicSource), + OutputDirectory: uncoerceString(fields.OutputDirectory, fromStringPointer(response.OutputDirectory)), + PublicSource: uncoerceBool(fields.PublicSource, fromBoolPointer(response.PublicSource)), RootDirectory: fromStringPointer(response.RootDirectory), ServerlessFunctionRegion: fromStringPointer(response.ServerlessFunctionRegion), - TeamID: teamID, + TeamID: types.String{Value: fields.TeamID.Value, Null: fields.TeamID.Null || fields.TeamID.Unknown}, } } diff --git a/vercel/resource_project_test.go b/vercel/resource_project_test.go index efa43d39..5e7cd4a4 100644 --- a/vercel/resource_project_test.go +++ b/vercel/resource_project_test.go @@ -260,6 +260,7 @@ func testAccProjectConfigWithGitRepoUpdated(projectSuffix, teamID string) string resource "vercel_project" "test_git" { name = "test-acc-two-%s" %s + public_source = false git_repository = { type = "github" repo = "%s"