From 8db0a33c6255b8dfd3312846e9f9e9270c56c502 Mon Sep 17 00:00:00 2001 From: Douglas Parsons Date: Tue, 19 Apr 2022 11:50:16 +0100 Subject: [PATCH] Remove file path trimming for root_directory settings - Add a `path_prefix` attribute to a deployment to indicate what fields should be trimmed. - Update documentation indicating the correct use of a project_directory. See https://github.com/vercel/terraform-provider-vercel/issues/14#issuecomment-1103973603 for a full rationale and explanation. Closes #14 --- docs/data-sources/file.md | 3 ++ docs/data-sources/project.md | 2 +- docs/data-sources/project_directory.md | 20 +++++++-- docs/resources/deployment.md | 18 +++++--- docs/resources/project.md | 15 +++---- .../data-sources/vercel_file/data-source.tf | 3 ++ .../vercel_project_directory/data-source.tf | 20 +++++++-- .../resources/vercel_deployment/resource.tf | 17 +++++--- vercel/data_source_project.go | 2 +- vercel/file_path.go | 18 -------- vercel/resource_deployment.go | 6 +++ vercel/resource_deployment_model.go | 25 ++++++++--- vercel/resource_deployment_test.go | 43 ++++++++++++++++--- vercel/resource_project.go | 17 +++----- vercel/resource_project_model.go | 19 ++------ 15 files changed, 141 insertions(+), 87 deletions(-) delete mode 100644 vercel/file_path.go diff --git a/docs/data-sources/file.md b/docs/data-sources/file.md index 26a2ae93..038e1b82 100644 --- a/docs/data-sources/file.md +++ b/docs/data-sources/file.md @@ -16,6 +16,9 @@ This will read a single file, providing metadata for use with a `vercel_deployme ## Example Usage ```terraform +# In this example, we are assuming that a single index.html file +# is being deployed. This file lives directly next to the terraform file. + data "vercel_file" "example" { path = "index.html" } diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index b7706871..6b62b13e 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -44,7 +44,7 @@ output "project_id" { - **build_command** (String) The build command for this project. If omitted, this value will be automatically detected. - **dev_command** (String) The dev command for this project. If omitted, this value will be automatically detected. -- **environment** (Attributes List) A list of environment variables that should be configured for the project. (see [below for nested schema](#nestedatt--environment)) +- **environment** (Attributes Set) A list of environment variables that should be configured for the project. (see [below for nested schema](#nestedatt--environment)) - **framework** (String) The framework that is being used for this project. If omitted, no framework is selected. - **git_repository** (Attributes) The Git Repository that will be connected to the project. When this is defined, any pushes to the specified connected Git Repository will be automatically deployed. This requires the corresponding Vercel for [Github](https://vercel.com/docs/concepts/git/vercel-for-github), [Gitlab](https://vercel.com/docs/concepts/git/vercel-for-gitlab) or [Bitbucket](https://vercel.com/docs/concepts/git/vercel-for-bitbucket) plugins to be installed. (see [below for nested schema](#nestedatt--git_repository)) - **id** (String) The ID of this resource. diff --git a/docs/data-sources/project_directory.md b/docs/data-sources/project_directory.md index 874ff200..890fa484 100644 --- a/docs/data-sources/project_directory.md +++ b/docs/data-sources/project_directory.md @@ -19,17 +19,31 @@ This will recursively read files, providing metadata for use with a `vercel_depl ## Example Usage ```terraform +# In this example, we are assuming that a nextjs UI +# exists in a `ui` directory and any terraform exists in a `terraform` directory. +# E.g. +# ``` +# ui/ +# src/ +# index.js +# package.json +# // etc... +# terraform/ +# main.tf +# ``` + data "vercel_project_directory" "example" { path = "../ui" } data "vercel_project" "example" { - name = "my-project" + name = "my-awesome-project" } resource "vercel_deployment" "example" { - project_id = data.vercel_project.example.id - files = data.vercel_project_directory.example.files + project_id = data.vercel_project.example.id + files = data.vercel_project_directory.example.files + path_prefix = data.vercel_project_directory.example.path } ``` diff --git a/docs/resources/deployment.md b/docs/resources/deployment.md index 1b88dc4f..77969305 100644 --- a/docs/resources/deployment.md +++ b/docs/resources/deployment.md @@ -26,18 +26,20 @@ Once the build step has completed successfully, a new, immutable deployment will ```terraform # In this example, we are assuming that a nextjs UI -# exists in a `ui` directory alongside any terraform. +# exists in a `ui` directory and any terraform exists in a `terraform` directory. # E.g. # ``` # ui/ # src/ -# next.config.js +# index.js +# package.json # // etc... -# main.tf +# terraform/ +# main.tf # ``` data "vercel_project_directory" "example" { - path = "ui" + path = "../ui" } data "vercel_project" "example" { @@ -45,9 +47,10 @@ data "vercel_project" "example" { } resource "vercel_deployment" "example" { - project_id = data.vercel_project.example.id - files = data.vercel_project_directory.example.files - production = true + project_id = data.vercel_project.example.id + files = data.vercel_project_directory.example.files + path_prefix = data.vercel_project_directory.example.path + production = true environment = { FOO = "bar" @@ -66,6 +69,7 @@ resource "vercel_deployment" "example" { ### Optional - **environment** (Map of String) A map of environment variable names to values. These are specific to a Deployment, and can also be configured on the `vercel_project` resource. +- **path_prefix** (String) If specified then the `path_prefix` will be stripped from the start of file paths as they are uploaded to Vercel. If this is omitted, then any leading `../`s will be stripped. - **production** (Boolean) true if the deployment is a production deployment, meaning production aliases will be assigned. - **project_settings** (Attributes) Project settings that will be applied to the deployment. (see [below for nested schema](#nestedatt--project_settings)) - **team_id** (String) The team ID to add the deployment to. diff --git a/docs/resources/project.md b/docs/resources/project.md index 66652218..9b97664c 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -6,11 +6,9 @@ description: |- Provides a Project resource. A Project groups deployments and custom domains. To deploy on Vercel, you need to create a Project. For more detailed information, please see the Vercel documentation https://vercel.com/docs/concepts/projects/overview. - -> The root_directory field behaves slightly differently to the Vercel website as - it allows upward path navigation (../). This is deliberately done so a vercel_file or vercel_project_directory - data source's path field can exactly match the root_directory. ~> If you are creating Deployments through terraform and intend to use both preview and production - deployments, you may not want to create a Project within the same terraform workspace as a Deployment. + deployments, you may wish to 'layer' your terraform, creating the Project with a different set of + terraform to your Deployment. --- # vercel_project (Resource) @@ -21,12 +19,9 @@ A Project groups deployments and custom domains. To deploy on Vercel, you need t For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/concepts/projects/overview). --> The `root_directory` field behaves slightly differently to the Vercel website as -it allows upward path navigation (`../`). This is deliberately done so a `vercel_file` or `vercel_project_directory` -data source's `path` field can exactly match the `root_directory`. - ~> If you are creating Deployments through terraform and intend to use both preview and production -deployments, you may not want to create a Project within the same terraform workspace as a Deployment. +deployments, you may wish to 'layer' your terraform, creating the Project with a different set of +terraform to your Deployment. ## Example Usage @@ -80,7 +75,7 @@ resource "vercel_project" "example" { - **build_command** (String) The build command for this project. If omitted, this value will be automatically detected. - **dev_command** (String) The dev command for this project. If omitted, this value will be automatically detected. -- **environment** (Attributes List) A list of environment variables that should be configured for the project. (see [below for nested schema](#nestedatt--environment)) +- **environment** (Attributes Set) A set of environment variables that should be configured for the project. (see [below for nested schema](#nestedatt--environment)) - **framework** (String) The framework that is being used for this project. If omitted, no framework is selected. - **git_repository** (Attributes) The Git Repository that will be connected to the project. When this is defined, any pushes to the specified connected Git Repository will be automatically deployed. This requires the corresponding Vercel for [Github](https://vercel.com/docs/concepts/git/vercel-for-github), [Gitlab](https://vercel.com/docs/concepts/git/vercel-for-gitlab) or [Bitbucket](https://vercel.com/docs/concepts/git/vercel-for-bitbucket) plugins to be installed. (see [below for nested schema](#nestedatt--git_repository)) - **install_command** (String) The install command for this project. If omitted, this value will be automatically detected. diff --git a/examples/data-sources/vercel_file/data-source.tf b/examples/data-sources/vercel_file/data-source.tf index f7242035..c4e68caf 100644 --- a/examples/data-sources/vercel_file/data-source.tf +++ b/examples/data-sources/vercel_file/data-source.tf @@ -1,3 +1,6 @@ +# In this example, we are assuming that a single index.html file +# is being deployed. This file lives directly next to the terraform file. + data "vercel_file" "example" { path = "index.html" } diff --git a/examples/data-sources/vercel_project_directory/data-source.tf b/examples/data-sources/vercel_project_directory/data-source.tf index 5c5971f7..ff6ba0d7 100644 --- a/examples/data-sources/vercel_project_directory/data-source.tf +++ b/examples/data-sources/vercel_project_directory/data-source.tf @@ -1,12 +1,26 @@ +# In this example, we are assuming that a nextjs UI +# exists in a `ui` directory and any terraform exists in a `terraform` directory. +# E.g. +# ``` +# ui/ +# src/ +# index.js +# package.json +# // etc... +# terraform/ +# main.tf +# ``` + data "vercel_project_directory" "example" { path = "../ui" } data "vercel_project" "example" { - name = "my-project" + name = "my-awesome-project" } resource "vercel_deployment" "example" { - project_id = data.vercel_project.example.id - files = data.vercel_project_directory.example.files + project_id = data.vercel_project.example.id + files = data.vercel_project_directory.example.files + path_prefix = data.vercel_project_directory.example.path } diff --git a/examples/resources/vercel_deployment/resource.tf b/examples/resources/vercel_deployment/resource.tf index a3e3e567..5e3635ed 100644 --- a/examples/resources/vercel_deployment/resource.tf +++ b/examples/resources/vercel_deployment/resource.tf @@ -1,16 +1,18 @@ # In this example, we are assuming that a nextjs UI -# exists in a `ui` directory alongside any terraform. +# exists in a `ui` directory and any terraform exists in a `terraform` directory. # E.g. # ``` # ui/ # src/ -# next.config.js +# index.js +# package.json # // etc... -# main.tf +# terraform/ +# main.tf # ``` data "vercel_project_directory" "example" { - path = "ui" + path = "../ui" } data "vercel_project" "example" { @@ -18,9 +20,10 @@ data "vercel_project" "example" { } resource "vercel_deployment" "example" { - project_id = data.vercel_project.example.id - files = data.vercel_project_directory.example.files - production = true + project_id = data.vercel_project.example.id + files = data.vercel_project_directory.example.files + path_prefix = data.vercel_project_directory.example.path + production = true environment = { FOO = "bar" diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index b5fecbe1..edd538d0 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -170,7 +170,7 @@ func (r dataSourceProject) Read(ctx context.Context, req tfsdk.ReadDataSourceReq return } - result := convertResponseToProject(out, config.TeamID, types.String{}) + result := convertResponseToProject(out, config.TeamID) tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, diff --git a/vercel/file_path.go b/vercel/file_path.go deleted file mode 100644 index f3706229..00000000 --- a/vercel/file_path.go +++ /dev/null @@ -1,18 +0,0 @@ -package vercel - -import "strings" - -// trimFilePath removes any upward directory navigations from the start of a file path. -// This is useful as Vercel doesn't allow a root directory that navigates upwards. -// But if we trim all file paths upwards, and we also trim the root directory, then -// all is good. -func trimFilePath(path string) string { - for strings.HasPrefix(path, "../") { - path = strings.TrimPrefix(path, "../") - } - if path == "" { - return "." - } - - return path -} diff --git a/vercel/resource_deployment.go b/vercel/resource_deployment.go index 9ecca0a2..730b24a1 100644 --- a/vercel/resource_deployment.go +++ b/vercel/resource_deployment.go @@ -60,6 +60,12 @@ Once the build step has completed successfully, a new, immutable deployment will Computed: true, Type: types.StringType, }, + "path_prefix": { + Description: "If specified then the `path_prefix` will be stripped from the start of file paths as they are uploaded to Vercel. If this is omitted, then any leading `../`s will be stripped.", + Optional: true, + Type: types.StringType, + PlanModifiers: tfsdk.AttributePlanModifiers{tfsdk.RequiresReplace()}, + }, "url": { Description: "A unique URL that is automatically generated for a deployment.", Computed: true, diff --git a/vercel/resource_deployment_model.go b/vercel/resource_deployment_model.go index fbc76a11..6f8d7902 100644 --- a/vercel/resource_deployment_model.go +++ b/vercel/resource_deployment_model.go @@ -28,6 +28,7 @@ type Deployment struct { ID types.String `tfsdk:"id"` Production types.Bool `tfsdk:"production"` ProjectID types.String `tfsdk:"project_id"` + PathPrefix types.String `tfsdk:"path_prefix"` ProjectSettings *ProjectSettings `tfsdk:"project_settings"` TeamID types.String `tfsdk:"team_id"` URL types.String `tfsdk:"url"` @@ -69,10 +70,6 @@ func (p *ProjectSettings) toRequest() map[string]interface{} { if p.RootDirectory.Null { res["rootDirectory"] = nil } - if p.RootDirectory.Value != "" { - v := trimFilePath(p.RootDirectory.Value) - res["rootDirectory"] = &v - } return res } @@ -118,13 +115,28 @@ func (d *Deployment) getFiles() ([]client.DeploymentFile, map[string]client.Depl } sha := sizeSha[1] + untrimmedFilename := filename + if d.PathPrefix.Unknown || d.PathPrefix.Null { + for strings.HasPrefix(filename, "../") { + filename = strings.TrimPrefix(filename, "../") + } + } else { + filename = strings.TrimPrefix(filename, d.PathPrefix.Value) + } file := client.DeploymentFile{ - File: trimFilePath(filename), + File: filename, Sha: sha, Size: size, } files = append(files, file) - filesBySha[sha] = file + + /* The API can return a set of missing files. When this happens, we want the path name + * complete with the original, untrimmed prefix. */ + filesBySha[sha] = client.DeploymentFile{ + File: untrimmedFilename, + Sha: sha, + Size: size, + } } return files, filesBySha, nil } @@ -171,6 +183,7 @@ func convertResponseToDeployment(response client.DeploymentResponse, plan Deploy URL: types.String{Value: response.URL}, Production: production, Files: plan.Files, + PathPrefix: fillStringNull(plan.PathPrefix), ProjectSettings: plan.ProjectSettings.fillNulls(), } } diff --git a/vercel/resource_deployment_test.go b/vercel/resource_deployment_test.go index 0258f4ad..dfc4359b 100644 --- a/vercel/resource_deployment_test.go +++ b/vercel/resource_deployment_test.go @@ -125,7 +125,7 @@ func TestAcc_DeploymentWithProjectSettings(t *testing.T) { }) } -func TestAcc_DeploymentWithUpwardRootDirectoryPath(t *testing.T) { +func TestAcc_DeploymentWithRootDirectoryOverride(t *testing.T) { projectSuffix := acctest.RandString(16) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -133,7 +133,7 @@ func TestAcc_DeploymentWithUpwardRootDirectoryPath(t *testing.T) { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccNonRootDirectory(projectSuffix), + Config: testAccRootDirectoryOverride(projectSuffix), Check: resource.ComposeAggregateTestCheckFunc( testAccDeploymentExists("vercel_deployment.test", ""), resource.TestCheckResourceAttr("vercel_deployment.test", "production", "true"), @@ -143,6 +143,23 @@ func TestAcc_DeploymentWithUpwardRootDirectoryPath(t *testing.T) { }) } +func TestAcc_DeploymentWithPathPrefix(t *testing.T) { + projectSuffix := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: noopDestroyCheck, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccRootDirectoryWithPathPrefix(projectSuffix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccDeploymentExists("vercel_deployment.test", ""), + ), + }, + }, + }) +} + func testAccDeployment(t *testing.T, tid string) { projectSuffix := acctest.RandString(16) extraConfig := "" @@ -197,11 +214,10 @@ resource "vercel_deployment" "test" { `, projectSuffix, projectExtras, deploymentExtras) } -func testAccNonRootDirectory(projectSuffix string) string { +func testAccRootDirectoryOverride(projectSuffix string) string { return fmt.Sprintf(` resource "vercel_project" "test" { name = "test-acc-deployment-%s" - root_directory = "../foo" } data "vercel_file" "index" { @@ -213,7 +229,24 @@ resource "vercel_deployment" "test" { files = data.vercel_file.index.file production = true project_settings = { - root_directory = "../vercel/example" + root_directory = "vercel/example" } }`, projectSuffix) } + +func testAccRootDirectoryWithPathPrefix(projectSuffix string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + name = "test-acc-deployment-%s" +} + +data "vercel_file" "index" { + path = "../vercel/example/index.html" +} + +resource "vercel_deployment" "test" { + project_id = vercel_project.test.id + files = data.vercel_file.index.file + path_prefix = "../vercel/example" +}`, projectSuffix) +} diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 3e5bf2f0..57364e45 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -26,12 +26,9 @@ A Project groups deployments and custom domains. To deploy on Vercel, you need t For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/concepts/projects/overview). --> The ` + "`root_directory`" + ` field behaves slightly differently to the Vercel website as -it allows upward path navigation (` + "`../`" + `). This is deliberately done so a ` + "`vercel_file` or `vercel_project_directory`" + ` -data source's ` + "`path`" + ` field can exactly match the ` + "`root_directory`" + `. - ~> If you are creating Deployments through terraform and intend to use both preview and production -deployments, you may not want to create a Project within the same terraform workspace as a Deployment. +deployments, you may wish to 'layer' your terraform, creating the Project with a different set of +terraform to your Deployment. `, Attributes: map[string]tfsdk.Attribute{ "team_id": { @@ -63,7 +60,7 @@ deployments, you may not want to create a Project within the same terraform work Description: "The dev command for this project. If omitted, this value will be automatically detected.", }, "environment": { - Description: "A list of environment variables that should be configured for the project.", + Description: "A set of environment variables that should be configured for the project.", Optional: true, Attributes: tfsdk.SetNestedAttributes(map[string]tfsdk.Attribute{ "target": { @@ -190,7 +187,7 @@ func (r resourceProject) Create(ctx context.Context, req tfsdk.CreateResourceReq return } - result := convertResponseToProject(out, plan.TeamID, plan.RootDirectory) + result := convertResponseToProject(out, plan.TeamID) tflog.Trace(ctx, "created project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, @@ -231,7 +228,7 @@ func (r resourceProject) Read(ctx context.Context, req tfsdk.ReadResourceRequest return } - result := convertResponseToProject(out, state.TeamID, state.RootDirectory) + result := convertResponseToProject(out, state.TeamID) tflog.Trace(ctx, "read project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, @@ -359,7 +356,7 @@ func (r resourceProject) Update(ctx context.Context, req tfsdk.UpdateResourceReq return } - result := convertResponseToProject(out, plan.TeamID, plan.RootDirectory) + result := convertResponseToProject(out, plan.TeamID) tflog.Trace(ctx, "updated project", map[string]interface{}{ "team_id": result.TeamID.Value, "project_id": result.ID.Value, @@ -449,7 +446,7 @@ func (r resourceProject) ImportState(ctx context.Context, req tfsdk.ImportResour if teamID == "" { stringTypeTeamID.Null = true } - result := convertResponseToProject(out, stringTypeTeamID, types.String{Unknown: true}) + result := convertResponseToProject(out, stringTypeTeamID) 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 7ae54772..2c30ea5e 100644 --- a/vercel/resource_project_model.go +++ b/vercel/resource_project_model.go @@ -40,14 +40,6 @@ func parseEnvironment(vars []EnvironmentItem) []client.EnvironmentVariable { return out } -func toStrPointerWithFileTrim(v types.String) *string { - if v.Null || v.Unknown { - return nil - } - trimmed := trimFilePath(v.Value) - return &trimmed -} - func (p *Project) toCreateProjectRequest() client.CreateProjectRequest { return client.CreateProjectRequest{ Name: p.Name.Value, @@ -59,7 +51,7 @@ func (p *Project) toCreateProjectRequest() client.CreateProjectRequest { InstallCommand: toStrPointer(p.InstallCommand), OutputDirectory: toStrPointer(p.OutputDirectory), PublicSource: toBoolPointer(p.PublicSource), - RootDirectory: toStrPointerWithFileTrim(p.RootDirectory), + RootDirectory: toStrPointer(p.RootDirectory), } } @@ -68,7 +60,6 @@ func (p *Project) toUpdateProjectRequest(oldName string) client.UpdateProjectReq if oldName != p.Name.Value { name = &p.Name.Value } - p.RootDirectory.Value = trimFilePath(p.RootDirectory.Value) return client.UpdateProjectRequest{ Name: name, BuildCommand: toStrPointer(p.BuildCommand), @@ -76,7 +67,7 @@ func (p *Project) toUpdateProjectRequest(oldName string) client.UpdateProjectReq Framework: toStrPointer(p.Framework), InstallCommand: toStrPointer(p.InstallCommand), OutputDirectory: toStrPointer(p.OutputDirectory), - RootDirectory: toStrPointerWithFileTrim(p.RootDirectory), + RootDirectory: toStrPointer(p.RootDirectory), PublicSource: toBoolPointer(p.PublicSource), } } @@ -119,7 +110,7 @@ func (g *GitRepository) toCreateProjectRequest() *client.GitRepository { } } -func convertResponseToProject(response client.ProjectResponse, tid, rootDir types.String) Project { +func convertResponseToProject(response client.ProjectResponse, tid types.String) Project { var gr *GitRepository if repo := response.Repository(); repo != nil { gr = &GitRepository{ @@ -145,10 +136,6 @@ func convertResponseToProject(response client.ProjectResponse, tid, rootDir type teamID.Null = true } - if response.RootDirectory != nil && trimFilePath(rootDir.Value) == *response.RootDirectory { - response.RootDirectory = &rootDir.Value - } - return Project{ TeamID: teamID, ID: types.String{Value: response.ID},