这是indexloc提供的服务,不要输入任何密码
Skip to content

Fix prebuilt deployment bugs #281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 98 additions & 54 deletions vercel/data_source_prebuilt_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"os"
Expand Down Expand Up @@ -181,8 +182,9 @@ func (d *prebuiltProjectDataSource) Read(ctx context.Context, req datasource.Rea
return
}

outputDir := filepath.Join(config.Path.ValueString(), ".vercel", "output")
validatePrebuiltOutput(&resp.Diagnostics, config.Path.ValueString())
projectPath := config.Path.ValueString()
outputDir := filepath.Join(projectPath, ".vercel", "output")
validatePrebuiltOutput(&resp.Diagnostics, projectPath)
if resp.Diagnostics.HasError() {
return
}
Expand All @@ -200,73 +202,48 @@ func (d *prebuiltProjectDataSource) Read(ctx context.Context, req datasource.Rea
return fmt.Errorf("could not get file info for %s: %w", path, err)
}

// If it's a symlink, resolve it
if info.Mode()&os.ModeSymlink != 0 {
dest, err := os.Readlink(path)
if err != nil {
return fmt.Errorf("could not read symlink %s: %w", path, err)
}
// Handle directories
if info.IsDir() {
return nil
}

// If the symlink is relative, make it absolute
if !filepath.IsAbs(dest) {
dest = filepath.Join(filepath.Dir(path), dest)
}
// Check if it's a symlink using Lstat
fileInfo, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("could not lstat file %s: %w", path, err)
}

destInfo, err := os.Stat(dest)
if fileInfo.Mode()&os.ModeSymlink != 0 {
// It's a symlink - read the target path
linkTarget, err := os.Readlink(path)
if err != nil {
return fmt.Errorf("could not stat symlink destination %s: %w", dest, err)
}

if destInfo.IsDir() {
return filepath.WalkDir(dest, func(subPath string, subD fs.DirEntry, subErr error) error {
if subErr != nil {
return subErr
}
if subD.IsDir() {
return nil
}
content, err := os.ReadFile(subPath)
if err != nil {
return fmt.Errorf("could not read file %s: %w", subPath, err)
}
rawSha := sha1.Sum(content)
sha := hex.EncodeToString(rawSha[:])

// Get the subpath relative to the symlink's destination directory
subRelPath, err := filepath.Rel(dest, subPath)
if err != nil {
return fmt.Errorf("could not get relative path: %w", err)
}

// Join it with the original symlink path
fullPath := filepath.Join(path, subRelPath)
config.Output[fullPath] = fmt.Sprintf("%d~%s", len(content), sha)
return nil
})
return fmt.Errorf("could not read symlink %s: %w", path, err)
}

// If it's a symlink to a file, read that file
content, err := os.ReadFile(dest)
if err != nil {
return fmt.Errorf("could not read symlinked file %s: %w", dest, err)
}
rawSha := sha1.Sum(content)
// Hash the link target string (just like Vercel does)
targetData := []byte(linkTarget)
rawSha := sha1.Sum(targetData)
sha := hex.EncodeToString(rawSha[:])
config.Output[path] = fmt.Sprintf("%d~%s", len(content), sha)
return nil
}

// Handle regular files (non-symlinks)
if !info.IsDir() {
config.Output[path] = fmt.Sprintf("%d~%s", len(targetData), sha)
} else {
// Regular file - read and hash its content
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("could not read file %s: %w", path, err)
}

rawSha := sha1.Sum(content)
sha := hex.EncodeToString(rawSha[:])
config.Output[path] = fmt.Sprintf("%d~%s", len(content), sha)
}

// If it's a .vc-config.json file, process it immediately
if filepath.Base(path) == ".vc-config.json" {
if err := processVCConfigFile(path, projectPath, &config); err != nil {
return fmt.Errorf("error processing .vc-config.json at %s: %w", path, err)
}
}

return nil
},
)
Expand All @@ -288,3 +265,70 @@ func (d *prebuiltProjectDataSource) Read(ctx context.Context, req datasource.Rea
return
}
}

// processVCConfigFile reads the .vc-config.json file and adds all files from filePathMap to the output
func processVCConfigFile(configPath, projectPath string, config *PrebuiltProjectData) error {
content, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("could not read .vc-config.json file: %w", err)
}

var vcConfig struct {
FilePathMap map[string]string `json:"filePathMap"`
}

if err := json.Unmarshal(content, &vcConfig); err != nil {
return fmt.Errorf("could not parse .vc-config.json: %w", err)
}

// Process each file in the filePathMap
for filePath := range vcConfig.FilePathMap {
// Don't process if we've already added this file
if _, exists := config.Output[filePath]; exists {
continue
}

// Make sure the path is absolute relative to the project
absPath := filePath
if !filepath.IsAbs(absPath) {
absPath = filepath.Join(projectPath, absPath)
}

// Check if file exists
fileInfo, err := os.Lstat(absPath) // Use Lstat to not follow symlinks
if err != nil {
return fmt.Errorf("could not stat file %s referenced in filePathMap: %w", absPath, err)
}

// Skip directories
if fileInfo.IsDir() {
continue
}

if fileInfo.Mode()&os.ModeSymlink != 0 {
// It's a symlink - read the target path
linkTarget, err := os.Readlink(absPath)
if err != nil {
return fmt.Errorf("could not read symlink %s: %w", absPath, err)
}

// Hash the link target string (just like Vercel does)
targetData := []byte(linkTarget)
rawSha := sha1.Sum(targetData)
sha := hex.EncodeToString(rawSha[:])
config.Output[filePath] = fmt.Sprintf("%d~%s", len(targetData), sha)
} else {
// Regular file - read and hash its content
fileContent, err := os.ReadFile(absPath)
if err != nil {
return fmt.Errorf("could not read file %s referenced in filePathMap: %w", absPath, err)
}

rawSha := sha1.Sum(fileContent)
sha := hex.EncodeToString(rawSha[:])
config.Output[filePath] = fmt.Sprintf("%d~%s", len(fileContent), sha)
}
}

return nil
}
79 changes: 72 additions & 7 deletions vercel/resource_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ func (p *ProjectSettings) fillNulls() *ProjectSettings {
}
}

func withSlashAtEndIfNeeded(path string) string {
if strings.HasSuffix(path, "/") {
return path
}
return fmt.Sprintf("%s/", path)
}

/*
* The files uploaded to Vercel need to have some minor adjustments:
* - Legacy behaviour was that any upward navigation ("../") was stripped from the
Expand All @@ -291,13 +298,13 @@ func normaliseFilename(filename string, pathPrefix types.String) string {
}
}

return strings.TrimPrefix(filename, filepath.ToSlash(pathPrefix.ValueString()))
return strings.TrimPrefix(filename, withSlashAtEndIfNeeded(filepath.ToSlash(pathPrefix.ValueString())))
}

// getFiles is a helper for turning the terraform deployment state into a set of client.DeploymentFile
// structs, ready to hit the API with. It also returns a map of files by sha, which is used to quickly
// look up any missing SHAs from the create deployment resposnse.
func getFiles(unparsedFiles map[string]string, pathPrefix types.String) ([]client.DeploymentFile, map[string]client.DeploymentFile, error) {
func getFiles(unparsedFiles map[string]string) ([]client.DeploymentFile, map[string]client.DeploymentFile, error) {
var files []client.DeploymentFile
filesBySha := map[string]client.DeploymentFile{}

Expand All @@ -313,7 +320,7 @@ func getFiles(unparsedFiles map[string]string, pathPrefix types.String) ([]clien
sha := sizeSha[1]

file := client.DeploymentFile{
File: normaliseFilename(filename, pathPrefix),
File: filename,
Sha: sha,
Size: size,
}
Expand Down Expand Up @@ -491,7 +498,7 @@ func (r *deploymentResource) Create(ctx context.Context, req resource.CreateRequ
if resp.Diagnostics.HasError() {
return
}
files, filesBySha, err := getFiles(unparsedFiles, plan.PathPrefix)
files, filesBySha, err := getFiles(unparsedFiles)
if err != nil {
resp.Diagnostics.AddError(
"Error creating deployment",
Expand All @@ -516,6 +523,10 @@ func (r *deploymentResource) Create(ctx context.Context, req resource.CreateRequ
if plan.Production.ValueBool() {
target = "production"
}
// normalise filenames.
for i := 0; i < len(files); i++ {
files[i].File = normaliseFilename(files[i].File, plan.PathPrefix)
}
cdr := client.CreateDeploymentRequest{
Files: files,
Environment: filterNullFromMap(environment),
Expand All @@ -540,24 +551,78 @@ func (r *deploymentResource) Create(ctx context.Context, req resource.CreateRequ
// Then we need to upload the files, and create the deployment again.
for _, sha := range mfErr.Missing {
f := filesBySha[sha]
content, err := os.ReadFile(f.File)

// Get file info to check if it's a symlink
fileInfo, err := os.Lstat(f.File)
if err != nil {
resp.Diagnostics.AddError(
"Error reading file",
"Error checking file",
fmt.Sprintf(
"Could not read file %s, unexpected error: %s",
"Could not get info for file %s, unexpected error: %s",
f.File,
err,
),
)
return
}

var content []byte

if fileInfo.Mode()&os.ModeSymlink != 0 {
// It's a symlink - read the target path
linkTarget, err := os.Readlink(f.File)
if err != nil {
resp.Diagnostics.AddError(
"Error reading symlink",
fmt.Sprintf(
"Could not read symlink %s, unexpected error: %s",
f.File,
err,
),
)
return
}
// For symlinks, the content is the target path as string
err = r.client.CreateFile(ctx, client.CreateFileRequest{
Filename: normaliseFilename(f.File, plan.PathPrefix),
SHA: f.Sha,
Content: linkTarget, // Just use the target path as content
TeamID: plan.TeamID.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
"Error uploading symlink file",
fmt.Sprintf(
"Could not upload symlink %s, unexpected error: %s",
f.File,
err,
),
)
return
}
} else {
// Regular file - read its content
content, err = os.ReadFile(f.File)
if err != nil {
resp.Diagnostics.AddError(
"Error reading file",
fmt.Sprintf(
"Could not read file %s, unexpected error: %s",
f.File,
err,
),
)
return
}
}

err = r.client.CreateFile(ctx, client.CreateFileRequest{
Filename: normaliseFilename(f.File, plan.PathPrefix),
SHA: f.Sha,
Content: string(content),
TeamID: plan.TeamID.ValueString(),
// If we need to preserve the file mode, add it here
// Mode: uint32(fileInfo.Mode()),
})
if err != nil {
resp.Diagnostics.AddError(
Expand Down