@@ -11,6 +11,7 @@ import (
1111 "log/slog"
1212 "os"
1313 "path"
14+ "regexp"
1415 "text/template"
1516
1617 "github.com/cloudbase/garm-provider-common/cloudconfig"
@@ -26,6 +27,7 @@ var Userdata embed.FS
2627
2728type WrapperContext struct {
2829 CallbackToken string
30+ CallbackURL string
2931 MetadataURL string
3032 CACertBundle string
3133}
@@ -74,8 +76,62 @@ func GetTemplateContent(osType commonParams.OSType, forge params.EndpointType) (
7476 return data , nil
7577}
7678
79+ // parseTemplate is the single entrypoint used to parse a runner install
80+ // template. Both rendering and validation go through it so that a template
81+ // which validates successfully is guaranteed to parse identically at render
82+ // time (same delimiters, same absence of a custom FuncMap). If the render path
83+ // ever gains custom functions or options, adding them here keeps validation
84+ // faithful automatically.
85+ func parseTemplate (tpl string ) (* template.Template , error ) {
86+ return template .New ("" ).Parse (tpl )
87+ }
88+
89+ // tmplParseErrRe matches the prefix text/template adds to parse errors, e.g.
90+ // `template: :3: unexpected "and"`. Our templates are unnamed, so the name
91+ // segment between the two colons is empty.
92+ var tmplParseErrRe = regexp .MustCompile (`^template: [^:]*:(\d+): *(.*)$` )
93+
94+ // cleanTemplateParseError rewrites the noisy `template: :<line>: <msg>` prefix
95+ // that text/template emits into a friendlier `line <n>: <msg>` form while
96+ // preserving the line number so callers (e.g. the web UI editor) can anchor a
97+ // diagnostic to it. Errors that don't match the expected shape are returned
98+ // verbatim.
99+ func cleanTemplateParseError (err error ) string {
100+ msg := err .Error ()
101+ if m := tmplParseErrRe .FindStringSubmatch (msg ); m != nil {
102+ return fmt .Sprintf ("line %s: %s" , m [1 ], m [2 ])
103+ }
104+ return msg
105+ }
106+
107+ // ValidateTemplate performs parse-time validation of a runner install template.
108+ //
109+ // It only parses the template; it deliberately does NOT execute it. Parsing
110+ // validates the grammar (delimiter balance, pipelines, block/end matching and
111+ // references to known functions) without ever inspecting the render context, so
112+ // it is completely independent of the variable data a template is rendered
113+ // against — most notably the per-pool ExtraSpecs/ExtraContext map. A reference
114+ // such as `{{ index .ExtraContext "gitea_cache_address" }}` is valid regardless
115+ // of which keys exist at render time. Executing to validate would be both
116+ // unsound (the context is unknown and variable at save time) and unnecessary
117+ // (a missing map key yields "" rather than an error), which is why parse-time
118+ // is exactly the right level.
119+ func ValidateTemplate (data []byte ) error {
120+ // Return typed bad-request errors (not plain fmt.Errorf) so that callers
121+ // which surface this directly get an HTTP 400 with the message in the
122+ // response body. A plain error would fall through handleError's default
123+ // case to an opaque 500 with the details stripped.
124+ if len (data ) == 0 {
125+ return runnerErrors .NewBadRequestError ("template data is empty" )
126+ }
127+ if _ , err := parseTemplate (string (data )); err != nil {
128+ return runnerErrors .NewBadRequestError ("invalid template: %s" , cleanTemplateParseError (err ))
129+ }
130+ return nil
131+ }
132+
77133func RenderRunnerInstallScript (tpl string , context any ) ([]byte , error ) {
78- t , err := template . New ( "" ). Parse (tpl )
134+ t , err := parseTemplate (tpl )
79135 if err != nil {
80136 return nil , fmt .Errorf ("failed to parse template: %w" , err )
81137 }
@@ -88,14 +144,15 @@ func RenderRunnerInstallScript(tpl string, context any) ([]byte, error) {
88144 return []byte (data ), nil
89145}
90146
91- func RenderRunnerInstallWrapper (ctx context.Context , osType commonParams.OSType , metadataURL , token string ) ([]byte , error ) {
147+ func RenderRunnerInstallWrapper (ctx context.Context , osType commonParams.OSType , metadataURL , callbackURL , token string ) ([]byte , error ) {
92148 tmpl , err := template .ParseFS (Userdata , "userdata/*_wrapper.tmpl" )
93149 if err != nil {
94150 return nil , fmt .Errorf ("failed to parse templates: %w" , err )
95151 }
96152
97153 templateCtx := WrapperContext {
98154 MetadataURL : metadataURL ,
155+ CallbackURL : callbackURL ,
99156 CallbackToken : token ,
100157 }
101158
0 commit comments