Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 74 additions & 0 deletions provisioner/terraform/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
e.mut.Lock()
defer e.mut.Unlock()

// Read .terraform.lock.hcl content before running terraform init
lockFilePath := getTerraformLockFilePath(e.workdir)
preInitLockFileContent, _ := os.ReadFile(lockFilePath)

outWriter, doneOut := logWriter(logr, proto.LogLevel_DEBUG)
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
defer func() {
Expand All @@ -242,6 +246,29 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
}

err := e.execWriteOutput(ctx, killCtx, args, e.basicEnv(), outWriter, errBuf)

// Check if .terraform.lock.hcl was modified after terraform init
postInitLockFileContent, _ := os.ReadFile(lockFilePath)
diff := generateFileDiff(preInitLockFileContent, postInitLockFileContent)
if diff != "" {
// Log informational message about lock file changes with diff
infoMsg := "INFO: .terraform.lock.hcl was modified during 'terraform init'. " +
"This is normal when Terraform downloads providers or updates dependencies. " +
"See https://developer.hashicorp.com/terraform/language/files/dependency-lock#understanding-lock-file-changes " +
"for more information about lock file changes."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that this is better left at WARN. If the lockfile is being modified, this potentially means that Coder is needlessly downloading providers from the internet instead of using cached versions, which means longer workspace build times.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point! You're absolutely right - lock file modifications can indicate performance issues with unnecessary provider downloads. I'll change it back to WARN level.


// Write info message to debug stream
if outWriter != nil {
_, _ = outWriter.Write([]byte(infoMsg + "\n"))
_, _ = outWriter.Write([]byte("\nLock file changes:\n" + diff + "\n"))
}

e.logger.Info(ctx, "terraform lock file modified during init",
slog.F("lock_file_path", lockFilePath),
slog.F("diff", diff),
)
}

var exitErr *exec.ExitError
if xerrors.As(err, &exitErr) {
if bytes.Contains(errBuf.b.Bytes(), []byte("text file busy")) {
Expand All @@ -259,6 +286,53 @@ func getStateFilePath(workdir string) string {
return filepath.Join(workdir, "terraform.tfstate")
}

func getTerraformLockFilePath(workdir string) string {
return filepath.Join(workdir, ".terraform.lock.hcl")
}

// generateFileDiff generates a simple diff between two file contents.
// Returns empty string if files are identical.
func generateFileDiff(beforeContent, afterContent []byte) string {
if bytes.Equal(beforeContent, afterContent) {
return ""
}

// Simple line-by-line diff
beforeLines := strings.Split(string(beforeContent), "\n")
afterLines := strings.Split(string(afterContent), "\n")

var diff strings.Builder
diff.WriteString("--- .terraform.lock.hcl (before terraform init)\n")
diff.WriteString("+++ .terraform.lock.hcl (after terraform init)\n")

// Simple diff showing added/removed lines
beforeMap := make(map[string]bool)
for _, line := range beforeLines {
beforeMap[line] = true
}

afterMap := make(map[string]bool)
for _, line := range afterLines {
afterMap[line] = true
}

// Show removed lines
for _, line := range beforeLines {
if !afterMap[line] && strings.TrimSpace(line) != "" {
diff.WriteString("- " + line + "\n")
}
}

// Show added lines
for _, line := range afterLines {
if !beforeMap[line] && strings.TrimSpace(line) != "" {
diff.WriteString("+ " + line + "\n")
}
}

return diff.String()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://pkg.go.dev/github.com/google/go-cmp/cmp#Diff
We already use this and its output is very readable!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent suggestion! Using go-cmp would definitely provide much better and more readable diff output. I'll switch to using cmp.Diff instead of the custom diff implementation.


// revive:disable-next-line:flag-parameter
func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, metadata *proto.Metadata) (*proto.PlanComplete, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
Expand Down
38 changes: 38 additions & 0 deletions provisioner/terraform/executor_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,41 @@ func TestOnlyDataResources(t *testing.T) {
})
}
}

func TestGetTerraformLockFilePath(t *testing.T) {
t.Parallel()

workdir := "/tmp/test"
expected := filepath.Join(workdir, ".terraform.lock.hcl")
got := getTerraformLockFilePath(workdir)
require.Equal(t, expected, got)
}

func TestGenerateFileDiff(t *testing.T) {
t.Parallel()

// Test with identical content
content := []byte("line1\nline2\nline3")
diff := generateFileDiff(content, content)
require.Equal(t, "", diff)

// Test with different content
content1 := []byte("line1\nline2\nline3")
content2 := []byte("line1\nmodified line2\nline3\nnew line4")
diff = generateFileDiff(content1, content2)
require.NotEmpty(t, diff)
require.Contains(t, diff, "--- .terraform.lock.hcl (before terraform init)")
require.Contains(t, diff, "+++ .terraform.lock.hcl (after terraform init)")
require.Contains(t, diff, "- line2")
require.Contains(t, diff, "+ modified line2")
require.Contains(t, diff, "+ new line4")

// Test with empty before content (new file)
emptyContent := []byte("")
newContent := []byte("provider \"aws\" {\n version = \"5.0.0\"\n}")
diff = generateFileDiff(emptyContent, newContent)
require.NotEmpty(t, diff)
require.Contains(t, diff, "+ provider \"aws\" {")
require.Contains(t, diff, "+ version = \"5.0.0\"")
require.Contains(t, diff, "+ }")
}