feat: add base template variables to API (#26425)

This commit is contained in:
Jeremy Ruppel
2026-06-17 12:22:35 -04:00
committed by GitHub
parent 6c95a618b4
commit 87de6dc23e
11 changed files with 258 additions and 54 deletions
+18
View File
@@ -23621,6 +23621,12 @@ const docTemplate = `{
},
"os": {
"type": "string"
},
"variables": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateBuilderModuleVariable"
}
}
}
},
@@ -23655,6 +23661,12 @@ const docTemplate = `{
"base_template_id": {
"type": "string"
},
"base_variable_values": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"modules": {
"type": "array",
"items": {
@@ -23684,6 +23696,12 @@ const docTemplate = `{
"base_template_id": {
"type": "string"
},
"base_variable_values": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"description": {
"type": "string"
},
+18
View File
@@ -21671,6 +21671,12 @@
},
"os": {
"type": "string"
},
"variables": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateBuilderModuleVariable"
}
}
}
},
@@ -21705,6 +21711,12 @@
"base_template_id": {
"type": "string"
},
"base_variable_values": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"modules": {
"type": "array",
"items": {
@@ -21731,6 +21743,12 @@
"base_template_id": {
"type": "string"
},
"base_variable_values": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"description": {
"type": "string"
},
+12
View File
@@ -41,6 +41,7 @@ type BaseManifest struct {
DisplayName string `json:"display_name"`
OS string `json:"os"`
DefaultContext BaseDefaultContext `json:"default_context"`
Variables []ModuleVariable `json:"variables"`
}
// BaseDefaultContext holds default render values stored in base.json.
@@ -192,6 +193,17 @@ func BaseTemplateIDs() []string {
return ids
}
// BaseVariables returns the user-facing variables for a given base
// template ID. Computed variables are excluded. Returns nil if the
// base is unknown or has no variables.
func BaseVariables(exampleID string) []ModuleVariable {
bases, err := loadBases()
if err != nil || bases[exampleID] == nil {
return nil
}
return bases[exampleID].Manifest.Variables
}
// BaseTemplateFS returns a filesystem rooted at the given base template
// directory within the embedded bases catalog. Returns an error if
// exampleID is not a known base template.
@@ -4,5 +4,24 @@
"os": "linux",
"default_context": {
"container_image": "codercom/enterprise-base:ubuntu"
}
},
"variables": [
{
"name": "use_kubeconfig",
"type": "bool",
"description": "Use host kubeconfig. Set to true if the Coder host is running outside the Kubernetes cluster for workspaces.",
"default": false,
"required": false,
"sensitive": false,
"computed": false
},
{
"name": "namespace",
"type": "string",
"description": "The Kubernetes namespace to create workspaces in. Must exist prior to creating workspaces.",
"required": true,
"sensitive": false,
"computed": false
}
]
}
+12 -3
View File
@@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"encoding/json"
"maps"
"regexp"
"strings"
"time"
@@ -21,6 +22,9 @@ var numberPattern = regexp.MustCompile(`^-?[0-9]+(\.[0-9]+)?$`)
// ComposeRequest describes which base template and modules to render.
type ComposeRequest struct {
BaseTemplateID string
// BaseVariableValues maps base template variable names to their
// user-supplied values.
BaseVariableValues map[string]string
// RegistryURL is the module registry base URL from the deployment
// config (CODER_TEMPLATE_BUILDER_REGISTRY_URL).
RegistryURL string
@@ -49,7 +53,7 @@ type ComposeResult struct {
// source files. It extracts the coder_agent resource name from the
// rendered base HCL and wires it into each module block.
func Compose(req ComposeRequest) (*ComposeResult, error) {
mainTF, err := renderBase(req.BaseTemplateID)
mainTF, err := renderBase(req.BaseTemplateID, req.BaseVariableValues)
if err != nil {
return nil, err
}
@@ -93,9 +97,14 @@ func formatHCL(src []byte) []byte {
return hclwrite.Format(src)
}
// renderBase renders the base template for the given example ID.
func renderBase(baseTemplateID string) ([]byte, error) {
// renderBase renders the base template for the given example ID,
// merging any user-supplied variable values into the render context.
func renderBase(baseTemplateID string, baseVars map[string]string) ([]byte, error) {
renderCtx := DefaultBaseRenderContext(baseTemplateID)
if renderCtx.Variables == nil {
renderCtx.Variables = make(map[string]string)
}
maps.Copy(renderCtx.Variables, baseVars)
mainTF, err := RenderBaseTemplate(baseTemplateID, "main.tf.tmpl", renderCtx)
if err != nil {
return nil, xerrors.Errorf("render base template: %w", err)
+28 -4
View File
@@ -72,12 +72,14 @@ func (api *API) templateBuilderBases(rw http.ResponseWriter, r *http.Request) {
slog.F("base_template_id", id))
continue
}
vars := baseVariablesToSDK(templatebuilder.BaseVariables(id))
bases = append(bases, codersdk.TemplateBuilderBase{
ID: ex.ID,
Name: ex.Name,
Description: ex.Description,
Icon: ex.Icon,
OS: string(templatebuilder.BaseTemplateOS(id)),
Variables: vars,
})
}
@@ -90,6 +92,26 @@ func (api *API) templateBuilderBases(rw http.ResponseWriter, r *http.Request) {
})
}
// baseVariablesToSDK converts base template variables to the SDK type,
// filtering out computed variables that the builder wires automatically.
func baseVariablesToSDK(vars []templatebuilder.ModuleVariable) []codersdk.TemplateBuilderModuleVariable {
out := make([]codersdk.TemplateBuilderModuleVariable, 0, len(vars))
for _, v := range vars {
if v.Computed {
continue
}
out = append(out, codersdk.TemplateBuilderModuleVariable{
Name: v.Name,
Type: codersdk.TemplateBuilderVariableType(v.Type),
Description: v.Description,
Default: v.Default,
Required: v.Required,
Sensitive: v.Sensitive,
})
}
return out
}
// @Summary List template builder modules
// @ID list-template-builder-modules
// @Security CoderSessionToken
@@ -171,8 +193,9 @@ func (api *API) templateBuilderCompose(rw http.ResponseWriter, r *http.Request)
}
composeReq := templatebuilder.ComposeRequest{
BaseTemplateID: req.BaseTemplateID,
RegistryURL: api.DeploymentValues.TemplateBuilder.RegistryURL.String(),
BaseTemplateID: req.BaseTemplateID,
BaseVariableValues: req.BaseVariableValues,
RegistryURL: api.DeploymentValues.TemplateBuilder.RegistryURL.String(),
}
for _, m := range req.Modules {
composeReq.Modules = append(composeReq.Modules, templatebuilder.ComposeModule{
@@ -280,8 +303,9 @@ func (api *API) templateBuilderCreateTemplate(rw http.ResponseWriter, r *http.Re
// Compose the template.
composeReq := templatebuilder.ComposeRequest{
BaseTemplateID: req.BaseTemplateID,
RegistryURL: api.DeploymentValues.TemplateBuilder.RegistryURL.String(),
BaseTemplateID: req.BaseTemplateID,
BaseVariableValues: req.BaseVariableValues,
RegistryURL: api.DeploymentValues.TemplateBuilder.RegistryURL.String(),
}
for _, m := range req.Modules {
composeReq.Modules = append(composeReq.Modules, templatebuilder.ComposeModule{
+47 -6
View File
@@ -34,12 +34,53 @@ func TestTemplateBuilderBases(t *testing.T) {
basesByID[b.ID] = b
}
for _, id := range templatebuilder.BaseTemplateIDs() {
b, ok := basesByID[id]
require.True(t, ok, "base %q missing from response", id)
require.NotEmpty(t, b.Name)
require.NotEmpty(t, b.Icon)
require.Equal(t, string(templatebuilder.BaseTemplateOS(id)), b.OS)
type baseSpec struct {
id string
expectedOS string
expectedVars []string
hasVariables bool
}
specs := []baseSpec{
{
id: "docker",
expectedOS: "linux",
hasVariables: false,
},
{
id: "kubernetes",
expectedOS: "linux",
hasVariables: true,
expectedVars: []string{"namespace", "use_kubeconfig"},
},
{
id: "aws-linux",
expectedOS: "linux",
hasVariables: false,
},
}
for _, spec := range specs {
b, ok := basesByID[spec.id]
require.True(t, ok, "base %q missing from response", spec.id)
require.NotEmpty(t, b.Name, "base %q should have a name", spec.id)
require.NotEmpty(t, b.Icon, "base %q should have an icon", spec.id)
require.Equal(t, spec.expectedOS, b.OS, "base %q OS mismatch", spec.id)
require.NotNil(t, b.Variables, "base %q should have non-nil variables slice", spec.id)
if spec.hasVariables {
require.NotEmpty(t, b.Variables, "base %q should have variables", spec.id)
varNames := make(map[string]bool, len(b.Variables))
for _, v := range b.Variables {
varNames[v.Name] = true
}
for _, expected := range spec.expectedVars {
require.True(t, varNames[expected],
"base %q should have variable %q", spec.id, expected)
}
} else {
require.Empty(t, b.Variables, "base %q should have no variables", spec.id)
}
}
})