mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
312 lines
8.6 KiB
Go
312 lines
8.6 KiB
Go
package templatebuilder_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/templatebuilder"
|
|
)
|
|
|
|
func TestClassifyProvisionerError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("NetworkErrors", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
jobError string
|
|
logs []string
|
|
}{
|
|
{
|
|
name: "DNSFailure",
|
|
jobError: "init failed",
|
|
logs: []string{"Error: Failed to query available provider packages", "dial tcp: lookup registry.terraform.io: no such host"},
|
|
},
|
|
{
|
|
name: "ConnectionRefused",
|
|
jobError: "terraform init: connection refused",
|
|
logs: nil,
|
|
},
|
|
{
|
|
name: "IOTimeout",
|
|
jobError: "context deadline exceeded",
|
|
logs: []string{"dial tcp 1.2.3.4:443: i/o timeout"},
|
|
},
|
|
{
|
|
name: "TLSTimeout",
|
|
jobError: "init error",
|
|
logs: []string{"net/http: TLS handshake timeout"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError(tc.jobError, tc.logs)
|
|
require.Contains(t, result, "unreachable from your provisioner")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("NetworkTakesPrecedenceOverAuth", func(t *testing.T) {
|
|
t.Parallel()
|
|
// When both network and auth patterns appear, network wins
|
|
// (checked first).
|
|
result := templatebuilder.ClassifyProvisionerError(
|
|
"connection refused",
|
|
[]string{"AccessDenied: check credentials"},
|
|
)
|
|
require.Contains(t, result, "unreachable from your provisioner")
|
|
})
|
|
|
|
t.Run("AuthErrors", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
jobError string
|
|
logs []string
|
|
}{
|
|
{
|
|
name: "AWSNoCredentials",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{
|
|
"Initializing the backend...",
|
|
"Initializing provider plugins...",
|
|
"Error: No valid credential sources found",
|
|
"",
|
|
" on main.tf line 10, in provider \"aws\":",
|
|
" 10: region = \"us-east-1\"",
|
|
},
|
|
},
|
|
{
|
|
name: "GCPDefaultCredentials",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{
|
|
"Error: could not find default credentials",
|
|
},
|
|
},
|
|
{
|
|
name: "AzureAuthFailure",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{"Error: AuthorizationFailed for subscription"},
|
|
},
|
|
{
|
|
name: "CaseInsensitive",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{"Error: ACCESSDENIED on resource"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError(tc.jobError, tc.logs)
|
|
require.Contains(t, result, "Cloud provider authentication failed")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("AuthErrorNoLogsNoDanglingNewlines", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Auth pattern in jobError with nil logs should not produce
|
|
// trailing newlines.
|
|
result := templatebuilder.ClassifyProvisionerError(
|
|
"AccessDenied: you are not allowed",
|
|
nil,
|
|
)
|
|
require.Contains(t, result, "Cloud provider authentication failed")
|
|
require.False(t, strings.HasSuffix(result, "\n"), "should not end with newline")
|
|
})
|
|
|
|
t.Run("UnrecognizedErrorIncludesLogs", func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError(
|
|
"terraform plan: exit status 1",
|
|
[]string{
|
|
"Initializing the backend...",
|
|
"Initializing provider plugins...",
|
|
"",
|
|
"Error: Unsupported block type",
|
|
"",
|
|
" on main.tf line 5, in resource \"null_resource\" \"test\":",
|
|
" 5: invalid_block {",
|
|
},
|
|
)
|
|
require.Contains(t, result, "terraform plan: exit status 1")
|
|
require.Contains(t, result, "Unsupported block type")
|
|
require.Contains(t, result, "on main.tf line 5")
|
|
})
|
|
|
|
t.Run("UnrecognizedErrorNoLogs", func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError(
|
|
"terraform plan: exit status 1",
|
|
nil,
|
|
)
|
|
require.Equal(t, "terraform plan: exit status 1", result)
|
|
})
|
|
|
|
t.Run("EmptyErrorPassthrough", func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError("", nil)
|
|
require.Equal(t, "", result)
|
|
})
|
|
|
|
t.Run("MultipleDiagnosticBlocks", func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError(
|
|
"terraform plan: exit status 1",
|
|
[]string{
|
|
"Warning: Deprecated resource",
|
|
" Use the new resource instead.",
|
|
"",
|
|
"Error: Invalid reference",
|
|
" on main.tf line 12:",
|
|
" 12: value = var.undefined",
|
|
},
|
|
)
|
|
require.Contains(t, result, "Deprecated resource")
|
|
require.Contains(t, result, "Invalid reference")
|
|
require.Contains(t, result, "on main.tf line 12")
|
|
})
|
|
|
|
t.Run("LogContextCappedAtMax", func(t *testing.T) {
|
|
t.Parallel()
|
|
var logs []string
|
|
for i := 0; i < 50; i++ {
|
|
logs = append(logs, "Error: problem number "+strings.Repeat("x", 10))
|
|
}
|
|
result := templatebuilder.ClassifyProvisionerError("exit status 1", logs)
|
|
parts := strings.SplitN(result, "\n\n", 2)
|
|
require.Len(t, parts, 2, "should have jobError and log context")
|
|
contextLines := strings.Split(strings.TrimSpace(parts[1]), "\n")
|
|
// 20 diagnostic lines + 1 truncation marker.
|
|
require.Equal(t, 21, len(contextLines))
|
|
require.Equal(t, "... (truncated)", contextLines[20])
|
|
})
|
|
|
|
t.Run("NonDiagnosticLogsFiltered", func(t *testing.T) {
|
|
t.Parallel()
|
|
result := templatebuilder.ClassifyProvisionerError(
|
|
"terraform plan: exit status 1",
|
|
[]string{
|
|
"Initializing the backend...",
|
|
"Initializing provider plugins...",
|
|
"- Finding latest version of hashicorp/aws...",
|
|
"- Installing hashicorp/aws v5.0.0...",
|
|
},
|
|
)
|
|
require.Equal(t, "terraform plan: exit status 1", result)
|
|
})
|
|
}
|
|
|
|
func TestClassifyProvisionerErrorCategory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
jobError string
|
|
logs []string
|
|
expected templatebuilder.ProvisionerErrorCategory
|
|
}{
|
|
{
|
|
name: "DNSFailure",
|
|
jobError: "init failed",
|
|
logs: []string{"dial tcp: lookup registry.terraform.io: no such host"},
|
|
expected: templatebuilder.ProvisionerErrorNetwork,
|
|
},
|
|
{
|
|
name: "TLSTimeout",
|
|
jobError: "init error",
|
|
logs: []string{"net/http: TLS handshake timeout"},
|
|
expected: templatebuilder.ProvisionerErrorNetwork,
|
|
},
|
|
{
|
|
name: "AWSNoCredentials",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{"Error: No valid credential sources found"},
|
|
expected: templatebuilder.ProvisionerErrorAuth,
|
|
},
|
|
{
|
|
name: "CaseInsensitiveAuth",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{"Error: ACCESSDENIED on resource"},
|
|
expected: templatebuilder.ProvisionerErrorAuth,
|
|
},
|
|
{
|
|
name: "NetworkTakesPrecedenceOverAuth",
|
|
jobError: "connection refused",
|
|
logs: []string{"AccessDenied: check credentials"},
|
|
expected: templatebuilder.ProvisionerErrorNetwork,
|
|
},
|
|
{
|
|
name: "TerraformSyntaxError",
|
|
jobError: "terraform plan: exit status 1",
|
|
logs: []string{"Error: Unsupported block type"},
|
|
expected: templatebuilder.ProvisionerErrorUnknown,
|
|
},
|
|
{
|
|
name: "Empty",
|
|
jobError: "",
|
|
logs: nil,
|
|
expected: templatebuilder.ProvisionerErrorUnknown,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tc.expected,
|
|
templatebuilder.ClassifyProvisionerErrorCategory(tc.jobError, tc.logs))
|
|
})
|
|
}
|
|
}
|
|
|
|
// The message shown to the user and the category reported as telemetry are
|
|
// derived from the same classification, so they must agree.
|
|
func TestClassifyProvisionerErrorMessageMatchesCategory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
jobError string
|
|
logs []string
|
|
category templatebuilder.ProvisionerErrorCategory
|
|
messageContains string
|
|
}{
|
|
{
|
|
name: "Network",
|
|
jobError: "terraform init: connection refused",
|
|
category: templatebuilder.ProvisionerErrorNetwork,
|
|
messageContains: "unreachable from your provisioner",
|
|
},
|
|
{
|
|
name: "Auth",
|
|
jobError: "AccessDenied: you are not allowed",
|
|
category: templatebuilder.ProvisionerErrorAuth,
|
|
messageContains: "Cloud provider authentication failed",
|
|
},
|
|
{
|
|
name: "Unknown",
|
|
jobError: "terraform plan: exit status 1",
|
|
category: templatebuilder.ProvisionerErrorUnknown,
|
|
messageContains: "terraform plan: exit status 1",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tc.category,
|
|
templatebuilder.ClassifyProvisionerErrorCategory(tc.jobError, tc.logs))
|
|
require.Contains(t,
|
|
templatebuilder.ClassifyProvisionerError(tc.jobError, tc.logs),
|
|
tc.messageContains)
|
|
})
|
|
}
|
|
}
|