package templatebuilder_test import ( "flag" "os" "path/filepath" "testing" "testing/fstest" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/templatebuilder" ) var updateGolden = flag.Bool("update", false, "update golden files") func TestRenderBaseTemplate(t *testing.T) { t.Parallel() t.Run("UnknownBase", func(t *testing.T) { t.Parallel() _, err := templatebuilder.RenderBaseTemplate("nonexistent", "main.tf.tmpl", templatebuilder.BaseRenderContext{}) require.Error(t, err) require.Contains(t, err.Error(), "unknown base template") }) t.Run("InvalidPath", func(t *testing.T) { t.Parallel() _, err := templatebuilder.RenderBaseTemplate("docker", "nonexistent.tf.tmpl", templatebuilder.BaseRenderContext{}) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) imageOpts := []templatebuilder.ImageOption{ {Name: "Ubuntu", Value: "codercom/enterprise-base:ubuntu"}, {Name: "Custom", Value: "custom/image:latest"}, } t.Run("DockerWithImageOptions", func(t *testing.T) { t.Parallel() renderCtx := templatebuilder.BaseRenderContext{ ContainerImage: "custom/image:latest", ImageOptions: imageOpts, } out, err := templatebuilder.RenderBaseTemplate("docker", "main.tf.tmpl", renderCtx) require.NoError(t, err) rendered := string(out) require.Contains(t, rendered, `data.coder_parameter.container_image.value`) require.Contains(t, rendered, `name = "Ubuntu"`) require.Contains(t, rendered, `name = "Custom"`) require.Contains(t, rendered, `coder_parameter`) }) t.Run("KubernetesWithImageOptions", func(t *testing.T) { t.Parallel() renderCtx := templatebuilder.BaseRenderContext{ ContainerImage: "custom/image:latest", ImageOptions: imageOpts, } out, err := templatebuilder.RenderBaseTemplate("kubernetes", "main.tf.tmpl", renderCtx) require.NoError(t, err) rendered := string(out) require.Contains(t, rendered, `data.coder_parameter.container_image.value`) require.Contains(t, rendered, `name = "Ubuntu"`) require.Contains(t, rendered, `coder_parameter`) }) // MissingKeyErrors is tested via RenderModuleTemplate since base templates // are pre-parsed from the embedded catalog and cannot use ad-hoc filesystems. } func TestRenderModuleTemplate(t *testing.T) { t.Parallel() t.Run("InvalidPath", func(t *testing.T) { t.Parallel() fsys := fstest.MapFS{} _, err := templatebuilder.RenderModuleTemplate(fsys, "missing.tf.tmpl", templatebuilder.ModuleRenderContext{}) require.Error(t, err) require.Contains(t, err.Error(), "read template") }) t.Run("RendersAllFields", func(t *testing.T) { t.Parallel() fsys := fstest.MapFS{ "test.tf.tmpl": &fstest.MapFile{ Data: []byte(`module "test" { source = "{{ .RegistryBase }}/coder/test/coder" version = "{{ .PinnedVersion }}" agent_id = coder_agent.{{ .AgentResourceName }}.id port = {{ .Variables.port }} } `), }, } ctx := templatebuilder.ModuleRenderContext{ RegistryBase: "https://registry.coder.com", PinnedVersion: "1.5.0", AgentResourceName: "main", Variables: map[string]string{"port": "8080"}, } out, err := templatebuilder.RenderModuleTemplate(fsys, "test.tf.tmpl", ctx) require.NoError(t, err) rendered := string(out) require.Contains(t, rendered, `"https://registry.coder.com/coder/test/coder"`) require.Contains(t, rendered, `"1.5.0"`) require.Contains(t, rendered, `coder_agent.main.id`) require.Contains(t, rendered, `port = 8080`) }) t.Run("NilVariablesDoesNotPanic", func(t *testing.T) { t.Parallel() fsys := fstest.MapFS{ "test.tf.tmpl": &fstest.MapFile{ Data: []byte(`module "test" { source = "{{ .RegistryBase }}" } `), }, } out, err := templatebuilder.RenderModuleTemplate(fsys, "test.tf.tmpl", templatebuilder.ModuleRenderContext{ RegistryBase: "https://registry.coder.com", }) require.NoError(t, err) require.Contains(t, string(out), "https://registry.coder.com") }) t.Run("MissingKeyErrors", func(t *testing.T) { t.Parallel() fsys := fstest.MapFS{ "test.tf.tmpl": &fstest.MapFile{ Data: []byte(`{{ .Variables.missing_key }}`), }, } _, err := templatebuilder.RenderModuleTemplate(fsys, "test.tf.tmpl", templatebuilder.ModuleRenderContext{ Variables: map[string]string{"other": "value"}, }) require.Error(t, err) require.Contains(t, err.Error(), "execute template") }) t.Run("ParseError", func(t *testing.T) { t.Parallel() fsys := fstest.MapFS{ "bad.tf.tmpl": &fstest.MapFile{ Data: []byte(`{{ .Invalid {{ syntax`), }, } _, err := templatebuilder.RenderModuleTemplate(fsys, "bad.tf.tmpl", templatebuilder.ModuleRenderContext{}) require.Error(t, err) require.Contains(t, err.Error(), "parse template") }) t.Run("RealModuleTemplate", func(t *testing.T) { t.Parallel() modules, err := templatebuilder.LoadModules() require.NoError(t, err) var csMod templatebuilder.ModuleManifest for _, m := range modules { if m.ID == "code-server" { csMod = m break } } require.NotEmpty(t, csMod.ID, "code-server module must exist") fsys, err := templatebuilder.ModuleTemplateFS(csMod.ID) require.NoError(t, err) vars := make(map[string]string) for _, v := range csMod.Variables { if !v.Computed && !v.Sensitive { vars[v.Name] = `"test-value"` } } ctx := templatebuilder.ModuleRenderContext{ RegistryBase: "https://registry.coder.com", PinnedVersion: csMod.PinnedVersion, AgentResourceName: "main", Variables: vars, } out, err := templatebuilder.RenderModuleTemplate(fsys, csMod.ID+".tf.tmpl", ctx) require.NoError(t, err) rendered := string(out) require.Contains(t, rendered, `module "code-server"`) require.Contains(t, rendered, `coder_agent.main.id`) require.Contains(t, rendered, csMod.PinnedVersion) }) } func TestExtractAgentResourceName(t *testing.T) { t.Parallel() t.Run("DockerBase", func(t *testing.T) { t.Parallel() rendered, err := templatebuilder.RenderBaseTemplate("docker", "main.tf.tmpl", templatebuilder.DefaultBaseRenderContext("docker")) require.NoError(t, err) name, err := templatebuilder.ExtractAgentResourceName(rendered) require.NoError(t, err) require.Equal(t, "main", name) }) t.Run("AWSLinuxBase", func(t *testing.T) { t.Parallel() rendered, err := templatebuilder.RenderBaseTemplate("aws-linux", "main.tf.tmpl", templatebuilder.DefaultBaseRenderContext("aws-linux")) require.NoError(t, err) name, err := templatebuilder.ExtractAgentResourceName(rendered) require.NoError(t, err) require.Equal(t, "dev", name) }) t.Run("NoAgent", func(t *testing.T) { t.Parallel() _, err := templatebuilder.ExtractAgentResourceName([]byte(`resource "docker_container" "workspace" {}`)) require.Error(t, err) require.Contains(t, err.Error(), "no coder_agent") }) t.Run("MultipleAgents", func(t *testing.T) { t.Parallel() hcl := []byte(` resource "coder_agent" "first" {} resource "coder_agent" "second" {} `) _, err := templatebuilder.ExtractAgentResourceName(hcl) require.Error(t, err) require.Contains(t, err.Error(), "expected exactly one") require.Contains(t, err.Error(), "found 2") }) t.Run("NilInput", func(t *testing.T) { t.Parallel() _, err := templatebuilder.ExtractAgentResourceName(nil) require.Error(t, err) require.Contains(t, err.Error(), "no coder_agent") }) t.Run("EmptyInput", func(t *testing.T) { t.Parallel() _, err := templatebuilder.ExtractAgentResourceName([]byte{}) require.Error(t, err) require.Contains(t, err.Error(), "no coder_agent") }) } func TestModuleTemplateFS(t *testing.T) { t.Parallel() t.Run("ValidModule", func(t *testing.T) { t.Parallel() fsys, err := templatebuilder.ModuleTemplateFS("code-server") require.NoError(t, err) require.NotNil(t, fsys) }) t.Run("UnknownModule", func(t *testing.T) { t.Parallel() _, err := templatebuilder.ModuleTemplateFS("nonexistent-module") require.Error(t, err) require.Contains(t, err.Error(), "not found in embedded catalog") }) } func TestBaseTemplateSnapshot(t *testing.T) { t.Parallel() tests := []struct { exampleID string }{ {exampleID: "docker"}, {exampleID: "kubernetes"}, {exampleID: "aws-linux"}, } for _, tc := range tests { t.Run(tc.exampleID, func(t *testing.T) { t.Parallel() renderCtx := templatebuilder.DefaultBaseRenderContext(tc.exampleID) rendered, err := templatebuilder.RenderBaseTemplate(tc.exampleID, "main.tf.tmpl", renderCtx) require.NoError(t, err) require.NotEmpty(t, rendered) goldenPath := filepath.Join("testdata", tc.exampleID+".tf.golden") if *updateGolden { err := os.MkdirAll("testdata", 0o755) require.NoError(t, err) err = os.WriteFile(goldenPath, rendered, 0o600) require.NoError(t, err) return } expected, err := os.ReadFile(goldenPath) require.NoError(t, err, "golden file %s not found; run with -update to create", goldenPath) require.Equal(t, string(expected), string(rendered), "rendered output for %s does not match golden file; run with -update to regenerate", tc.exampleID) }) } }