-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathrender.go
More file actions
138 lines (119 loc) · 4.62 KB
/
Copy pathrender.go
File metadata and controls
138 lines (119 loc) · 4.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package templatebuilder
import (
"bytes"
"io/fs"
"regexp"
"text/template"
"golang.org/x/xerrors"
)
// ImageOption represents a container image choice for base template parameters.
type ImageOption struct {
Name string
Value string
}
// BaseRenderContext is the data passed to base template .tf.tmpl files.
type BaseRenderContext struct {
ContainerImage string
ImageOptions []ImageOption
Variables map[string]string
}
// ModuleRenderContext is the data passed to module .tf.tmpl files.
type ModuleRenderContext struct {
// RegistryBase is the module registry URL from the deployment config
// (CODER_TEMPLATE_BUILDER_REGISTRY_URL).
RegistryBase string
// PinnedVersion is the module version from the catalog manifest.
PinnedVersion string
// AgentResourceName is the Terraform resource name of the coder_agent
// declared in the base template (e.g. "main" or "dev").
AgentResourceName string
// Variables maps variable names to their HCL expressions.
Variables map[string]string
}
// RenderBaseTemplate executes a pre-parsed .tf.tmpl template for the given
// base, applying the provided render context. Templates are parsed once at
// first access via sync.OnceValues, so parse errors surface early instead
// of at render time.
func RenderBaseTemplate(exampleID, templatePath string, renderCtx BaseRenderContext) ([]byte, error) {
if renderCtx.Variables == nil {
renderCtx.Variables = make(map[string]string)
}
bases, err := loadBases()
if err != nil {
return nil, xerrors.Errorf("load base catalog: %w", err)
}
base, ok := bases[exampleID]
if !ok {
return nil, xerrors.Errorf("unknown base template %q", exampleID)
}
tmpl, ok := base.Templates[templatePath]
if !ok {
return nil, xerrors.Errorf("template %s not found in base %q", templatePath, exampleID)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, renderCtx); err != nil {
return nil, xerrors.Errorf("execute template %s: %w", templatePath, err)
}
return buf.Bytes(), nil
}
// RenderModuleTemplate parses and executes a module .tf.tmpl file from
// the given filesystem, applying the provided render context.
func RenderModuleTemplate(fsys fs.FS, templatePath string, renderCtx ModuleRenderContext) ([]byte, error) {
if renderCtx.Variables == nil {
renderCtx.Variables = make(map[string]string)
}
return renderTemplate(fsys, templatePath, renderCtx)
}
// renderTemplate is the shared implementation for module template rendering.
// It sets missingkey=error so that references to undefined variable keys fail
// loudly instead of producing "<no value>".
func renderTemplate(fsys fs.FS, templatePath string, data any) ([]byte, error) {
raw, err := fs.ReadFile(fsys, templatePath)
if err != nil {
return nil, xerrors.Errorf("read template %s: %w", templatePath, err)
}
tmpl, err := template.New(templatePath).Option("missingkey=error").Parse(string(raw))
if err != nil {
return nil, xerrors.Errorf("parse template %s: %w", templatePath, err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, xerrors.Errorf("execute template %s: %w", templatePath, err)
}
return buf.Bytes(), nil
}
// agentResourcePattern matches `resource "coder_agent" "<name>"` in HCL.
var agentResourcePattern = regexp.MustCompile(`resource\s+"coder_agent"\s+"(\w+)"`)
// agentCountPattern detects whether a coder_agent block uses count or
// for_each, which means references to it require an index (e.g. [0]).
var agentCountPattern = regexp.MustCompile(
`resource\s+"coder_agent"\s+"\w+"\s*\{[^}]*\b(?:count|for_each)\s*=`,
)
// ExtractAgentResourceName finds the coder_agent resource declaration in
// rendered HCL and returns the reference form to use in module templates.
// When the agent uses count or for_each, the returned name includes an
// index suffix (e.g. "dev[0]") so that module templates can reference it
// as coder_agent.<name>.id. Returns an error unless exactly one
// coder_agent resource is found; the builder only supports single-agent
// templates. The input is expected to be rendered output from our own
// curated base templates, not arbitrary user HCL.
func ExtractAgentResourceName(hcl []byte) (string, error) {
matches := agentResourcePattern.FindAllSubmatch(hcl, -1)
switch len(matches) {
case 0:
return "", xerrors.New("no coder_agent resource found in rendered template")
case 1:
name := string(matches[0][1])
if agentCountPattern.Match(hcl) {
name += "[0]"
}
return name, nil
default:
names := make([]string, 0, len(matches))
for _, m := range matches {
names = append(names, string(m[1]))
}
return "", xerrors.Errorf("expected exactly one coder_agent resource, found %d: %v",
len(matches), names)
}
}