Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a48ace5

Browse filesBrowse files
authored
fix(coderd/templatebuilder): include Terraform diagnostics in import errors (#26677)
`ClassifyProvisionerError` previously returned only the raw job error string (e.g. "terraform plan: exit status 1") for unrecognized failures, discarding the provisioner log lines it already had. This made template import errors in the builder unactionable. Now the function extracts Terraform diagnostic blocks (Error:/Warning: lines and their context) from the provisioner logs and appends them to the error detail. This surfaces the actual failure cause (missing credentials, invalid references, unsupported blocks) in the ErrorAlert banner. Changes: - `extractDiagnostics` parses Terraform diagnostic blocks from log output, capped at 20 lines with a truncation marker - Auth error classification for AWS, GCP, Azure credential failures with a targeted user-facing message - Case-insensitive pattern matching via `strings.ToLower` on the combined text - Auth branch guarded against empty diagnostics (no trailing `\n\n`) > [!NOTE] > This PR was authored by Coder Agents on behalf of @jeremyruppel.
1 parent c40b3b9 commit a48ace5
Copy full SHA for a48ace5

3 files changed

+321-62Lines changed: 321 additions & 62 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎coderd/templatebuilder/compose_test.go‎

Copy file name to clipboardExpand all lines: coderd/templatebuilder/compose_test.go
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ func TestCompose(t *testing.T) {
9999
require.Contains(t, result.ExtraFiles, "cloud-init/cloud-config.yaml.tftpl")
100100
})
101101

102+
t.Run("GCPWindowsBase", func(t *testing.T) {
103+
t.Parallel()
104+
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
105+
BaseTemplateID: "gcp-windows",
106+
RegistryURL: "https://registry.coder.com",
107+
})
108+
require.NoError(t, err)
109+
require.NotEmpty(t, result.MainTF)
110+
require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`)
111+
})
112+
113+
t.Run("AzureLinuxExtraFiles", func(t *testing.T) {
114+
t.Parallel()
115+
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
116+
BaseTemplateID: "azure-linux",
117+
RegistryURL: "https://registry.coder.com",
118+
})
119+
require.NoError(t, err)
120+
require.Contains(t, result.ExtraFiles, "cloud-init/cloud-config.yaml.tftpl")
121+
})
122+
102123
t.Run("SensitiveVariable", func(t *testing.T) {
103124
t.Parallel()
104125
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
Collapse file

‎coderd/templatebuilder/errors.go‎

Copy file name to clipboardExpand all lines: coderd/templatebuilder/errors.go
+109-4Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,45 @@ package templatebuilder
33
import "strings"
44

55
// networkErrorPatterns are substrings found in provisioner job output when
6-
// the Terraform registry or provider endpoints are unreachable.
6+
// the Terraform registry or provider endpoints are unreachable. These are
7+
// intentionally not augmented with diagnostic context because the canned
8+
// message is self-explanatory and the underlying logs rarely add value.
79
var networkErrorPatterns = []string{
810
"no such host",
911
"connection refused",
1012
"i/o timeout",
1113
"dial tcp: lookup",
1214
"network is unreachable",
1315
"no route to host",
14-
"TLS handshake timeout",
16+
"tls handshake timeout",
1517
}
1618

19+
// authErrorPatterns are substrings that indicate cloud provider
20+
// authentication or credential failures. Matching is case-insensitive
21+
// (the combined text is lowercased before comparison).
22+
var authErrorPatterns = []string{
23+
"no valid credential sources found",
24+
"could not find default credentials",
25+
"unauthorizedaccess",
26+
"authorizationfailed",
27+
"authfailure",
28+
"accessdenied",
29+
"invalidclienttokenid",
30+
"expiredtoken",
31+
"signaturedoesnotmatch",
32+
"not authorized to perform",
33+
}
34+
35+
// maxLogContextLines caps how many relevant log lines are included in
36+
// the error detail to avoid returning excessive output.
37+
const maxLogContextLines = 20
38+
1739
// ClassifyProvisionerError inspects a provisioner job error and its log
1840
// lines, returning a user-friendly message for known failure modes.
19-
// If the error is not recognized, the raw jobError is returned unchanged.
41+
// For unrecognized errors, the raw jobError is returned with relevant
42+
// log context appended so the user can diagnose the failure.
2043
func ClassifyProvisionerError(jobError string, logs []string) string {
21-
combined := jobError + "\n" + strings.Join(logs, "\n")
44+
combined := strings.ToLower(jobError + "\n" + strings.Join(logs, "\n"))
2245

2346
for _, pattern := range networkErrorPatterns {
2447
if strings.Contains(combined, pattern) {
@@ -28,5 +51,87 @@ func ClassifyProvisionerError(jobError string, logs []string) string {
2851
}
2952
}
3053

54+
for _, pattern := range authErrorPatterns {
55+
if strings.Contains(combined, pattern) {
56+
msg := "Cloud provider authentication failed. " +
57+
"Check that valid credentials are configured for the provisioner."
58+
if diag := extractDiagnostics(logs); diag != "" {
59+
msg += "\n\n" + diag
60+
}
61+
return msg
62+
}
63+
}
64+
65+
// For unrecognized errors, include relevant Terraform output so the
66+
// user can diagnose the failure.
67+
if diag := extractDiagnostics(logs); diag != "" {
68+
return jobError + "\n\n" + diag
69+
}
3170
return jobError
3271
}
72+
73+
// diagnosticPrefixes identify Terraform diagnostic output lines worth
74+
// surfacing. Terraform formats errors as blocks starting with "Error:"
75+
// or "Warning:" followed by indented detail lines.
76+
var diagnosticPrefixes = []string{
77+
"Error:",
78+
"Warning:",
79+
"error:",
80+
"warning:",
81+
}
82+
83+
// isDiagnosticStart returns true if the line begins a Terraform
84+
// diagnostic block.
85+
func isDiagnosticStart(line string) bool {
86+
for _, prefix := range diagnosticPrefixes {
87+
if strings.HasPrefix(line, prefix) {
88+
return true
89+
}
90+
}
91+
return false
92+
}
93+
94+
// extractDiagnostics pulls Terraform diagnostic blocks from provisioner
95+
// log output. It keeps lines that start a diagnostic (Error:/Warning:)
96+
// and subsequent indented or context lines, up to maxLogContextLines.
97+
func extractDiagnostics(logs []string) string {
98+
var lines []string
99+
inBlock := false
100+
101+
for _, line := range logs {
102+
if len(lines) >= maxLogContextLines {
103+
lines = append(lines, "... (truncated)")
104+
break
105+
}
106+
107+
trimmed := strings.TrimSpace(line)
108+
if trimmed == "" {
109+
if inBlock {
110+
// Blank line inside a block: keep it to preserve
111+
// readability, but end the block.
112+
lines = append(lines, "")
113+
inBlock = false
114+
}
115+
continue
116+
}
117+
118+
if isDiagnosticStart(trimmed) {
119+
inBlock = true
120+
lines = append(lines, trimmed)
121+
continue
122+
}
123+
124+
if inBlock {
125+
lines = append(lines, trimmed)
126+
continue
127+
}
128+
129+
// Capture "on <file> line <N>" references that appear
130+
// outside the main diagnostic block in some Terraform versions.
131+
if strings.HasPrefix(trimmed, "on ") && strings.Contains(trimmed, " line ") {
132+
lines = append(lines, trimmed)
133+
}
134+
}
135+
136+
return strings.Join(lines, "\n")
137+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.