From 87de6dc23e02102ad2cf9d266ead7ffbd0278df5 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Wed, 17 Jun 2026 12:22:35 -0400 Subject: [PATCH] feat: add base template variables to API (#26425) --- coderd/apidoc/docs.go | 18 ++++ coderd/apidoc/swagger.json | 18 ++++ coderd/templatebuilder/bases.go | 12 +++ .../bases/kubernetes/base.json | 21 ++++- coderd/templatebuilder/compose.go | 15 +++- coderd/templatebuilder_handler.go | 32 ++++++- coderd/templatebuilder_handler_test.go | 53 ++++++++++-- codersdk/templatebuilder.go | 33 +++---- docs/reference/api/schemas.md | 85 +++++++++++++------ docs/reference/api/templatebuilder.md | 22 ++++- site/src/api/typesGenerated.ts | 3 + 11 files changed, 258 insertions(+), 54 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a017de9c24..24eaec0941 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index bcdee7377d..26c4aff908 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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" }, diff --git a/coderd/templatebuilder/bases.go b/coderd/templatebuilder/bases.go index 90d91f32a4..093b632a5d 100644 --- a/coderd/templatebuilder/bases.go +++ b/coderd/templatebuilder/bases.go @@ -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. diff --git a/coderd/templatebuilder/bases/kubernetes/base.json b/coderd/templatebuilder/bases/kubernetes/base.json index 0baf12385f..a3b04acbaa 100644 --- a/coderd/templatebuilder/bases/kubernetes/base.json +++ b/coderd/templatebuilder/bases/kubernetes/base.json @@ -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 + } + ] } diff --git a/coderd/templatebuilder/compose.go b/coderd/templatebuilder/compose.go index e3af4db4a4..b3df18ab2d 100644 --- a/coderd/templatebuilder/compose.go +++ b/coderd/templatebuilder/compose.go @@ -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) diff --git a/coderd/templatebuilder_handler.go b/coderd/templatebuilder_handler.go index b0c23fe0e2..3c18e19bee 100644 --- a/coderd/templatebuilder_handler.go +++ b/coderd/templatebuilder_handler.go @@ -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{ diff --git a/coderd/templatebuilder_handler_test.go b/coderd/templatebuilder_handler_test.go index fdcf651606..98796db425 100644 --- a/coderd/templatebuilder_handler_test.go +++ b/coderd/templatebuilder_handler_test.go @@ -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) + } } }) diff --git a/codersdk/templatebuilder.go b/codersdk/templatebuilder.go index 90e7068520..32406752b6 100644 --- a/codersdk/templatebuilder.go +++ b/codersdk/templatebuilder.go @@ -52,11 +52,12 @@ type TemplateBuilderModulesResponse struct { // TemplateBuilderBase is the API response type for a base template // returned by GET /api/v2/templatebuilder/bases. type TemplateBuilderBase struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Icon string `json:"icon"` - OS string `json:"os"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + OS string `json:"os"` + Variables []TemplateBuilderModuleVariable `json:"variables"` } // TemplateBuilderBasesResponse is the response body for listing template builder bases. @@ -102,8 +103,9 @@ func (c *Client) TemplateBuilderModules(ctx context.Context, base string) (Templ // TemplateBuilderComposeRequest is the request body for // POST /api/v2/templatebuilder/compose. type TemplateBuilderComposeRequest struct { - BaseTemplateID string `json:"base_template_id"` - Modules []TemplateBuilderComposeModule `json:"modules"` + BaseTemplateID string `json:"base_template_id"` + BaseVariableValues map[string]string `json:"base_variable_values,omitempty"` + Modules []TemplateBuilderComposeModule `json:"modules"` } // TemplateBuilderComposeModule identifies a module and its variable @@ -130,14 +132,15 @@ func (c *Client) TemplateBuilderCompose(ctx context.Context, req TemplateBuilder // TemplateBuilderCreateTemplateRequest is the request body for // POST /api/v2/templatebuilder/compose/template. type TemplateBuilderCreateTemplateRequest struct { - BaseTemplateID string `json:"base_template_id"` - Modules []TemplateBuilderComposeModule `json:"modules"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid" validate:"required"` - Name string `json:"name" validate:"required,template_name"` - DisplayName string `json:"display_name,omitempty" validate:"template_display_name"` - Description string `json:"description,omitempty" validate:"lt=128"` - Icon string `json:"icon,omitempty"` - ProvisionerTags map[string]string `json:"provisioner_tags,omitempty"` + BaseTemplateID string `json:"base_template_id"` + BaseVariableValues map[string]string `json:"base_variable_values,omitempty"` + Modules []TemplateBuilderComposeModule `json:"modules"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid" validate:"required"` + Name string `json:"name" validate:"required,template_name"` + DisplayName string `json:"display_name,omitempty" validate:"template_display_name"` + Description string `json:"description,omitempty" validate:"lt=128"` + Icon string `json:"icon,omitempty"` + ProvisionerTags map[string]string `json:"provisioner_tags,omitempty"` } // TemplateBuilderCreateTemplateResponse is the response body for diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index cf117d6509..8998babc87 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -11883,19 +11883,32 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "icon": "string", "id": "string", "name": "string", - "os": "string" + "os": "string", + "variables": [ + { + "default": [ + 0 + ], + "description": "string", + "name": "string", + "required": true, + "sensitive": true, + "type": "string" + } + ] } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------|--------|----------|--------------|-------------| -| `description` | string | false | | | -| `icon` | string | false | | | -| `id` | string | false | | | -| `name` | string | false | | | -| `os` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------|-------------------------------------------------------------------------------------------|----------|--------------|-------------| +| `description` | string | false | | | +| `icon` | string | false | | | +| `id` | string | false | | | +| `name` | string | false | | | +| `os` | string | false | | | +| `variables` | array of [codersdk.TemplateBuilderModuleVariable](#codersdktemplatebuildermodulevariable) | false | | | ## codersdk.TemplateBuilderBasesResponse @@ -11907,7 +11920,19 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "icon": "string", "id": "string", "name": "string", - "os": "string" + "os": "string", + "variables": [ + { + "default": [ + 0 + ], + "description": "string", + "name": "string", + "required": true, + "sensitive": true, + "type": "string" + } + ] } ] } @@ -11944,6 +11969,10 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ```json { "base_template_id": "string", + "base_variable_values": { + "property1": "string", + "property2": "string" + }, "modules": [ { "id": "string", @@ -11958,10 +11987,12 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|-----------------------------------------------------------------------------------------|----------|--------------|-------------| -| `base_template_id` | string | false | | | -| `modules` | array of [codersdk.TemplateBuilderComposeModule](#codersdktemplatebuildercomposemodule) | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|-----------------------------------------------------------------------------------------|----------|--------------|-------------| +| `base_template_id` | string | false | | | +| `base_variable_values` | object | false | | | +| » `[any property]` | string | false | | | +| `modules` | array of [codersdk.TemplateBuilderComposeModule](#codersdktemplatebuildercomposemodule) | false | | | ## codersdk.TemplateBuilderConfig @@ -11984,6 +12015,10 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ```json { "base_template_id": "string", + "base_variable_values": { + "property1": "string", + "property2": "string" + }, "description": "string", "display_name": "string", "icon": "string", @@ -12007,17 +12042,19 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|-----------------------------------------------------------------------------------------|----------|--------------|-------------| -| `base_template_id` | string | false | | | -| `description` | string | false | | | -| `display_name` | string | false | | | -| `icon` | string | false | | | -| `modules` | array of [codersdk.TemplateBuilderComposeModule](#codersdktemplatebuildercomposemodule) | false | | | -| `name` | string | true | | | -| `organization_id` | string | true | | | -| `provisioner_tags` | object | false | | | -| » `[any property]` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|-----------------------------------------------------------------------------------------|----------|--------------|-------------| +| `base_template_id` | string | false | | | +| `base_variable_values` | object | false | | | +| » `[any property]` | string | false | | | +| `description` | string | false | | | +| `display_name` | string | false | | | +| `icon` | string | false | | | +| `modules` | array of [codersdk.TemplateBuilderComposeModule](#codersdktemplatebuildercomposemodule) | false | | | +| `name` | string | true | | | +| `organization_id` | string | true | | | +| `provisioner_tags` | object | false | | | +| » `[any property]` | string | false | | | ## codersdk.TemplateBuilderCreateTemplateResponse diff --git a/docs/reference/api/templatebuilder.md b/docs/reference/api/templatebuilder.md index 8c55750264..05ba6cc720 100644 --- a/docs/reference/api/templatebuilder.md +++ b/docs/reference/api/templatebuilder.md @@ -25,7 +25,19 @@ curl -X GET http://coder-server:8080/api/v2/templatebuilder/bases \ "icon": "string", "id": "string", "name": "string", - "os": "string" + "os": "string", + "variables": [ + { + "default": [ + 0 + ], + "description": "string", + "name": "string", + "required": true, + "sensitive": true, + "type": "string" + } + ] } ] } @@ -57,6 +69,10 @@ curl -X POST http://coder-server:8080/api/v2/templatebuilder/compose \ ```json { "base_template_id": "string", + "base_variable_values": { + "property1": "string", + "property2": "string" + }, "modules": [ { "id": "string", @@ -102,6 +118,10 @@ curl -X POST http://coder-server:8080/api/v2/templatebuilder/compose/template \ ```json { "base_template_id": "string", + "base_variable_values": { + "property1": "string", + "property2": "string" + }, "description": "string", "display_name": "string", "icon": "string", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5ef2e86e3f..9b580dfec6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -8119,6 +8119,7 @@ export interface TemplateBuilderBase { readonly description: string; readonly icon: string; readonly os: string; + readonly variables: readonly TemplateBuilderModuleVariable[]; } // From codersdk/templatebuilder.go @@ -8146,6 +8147,7 @@ export interface TemplateBuilderComposeModule { */ export interface TemplateBuilderComposeRequest { readonly base_template_id: string; + readonly base_variable_values?: Record; readonly modules: readonly TemplateBuilderComposeModule[]; } @@ -8162,6 +8164,7 @@ export interface TemplateBuilderConfig { */ export interface TemplateBuilderCreateTemplateRequest { readonly base_template_id: string; + readonly base_variable_values?: Record; readonly modules: readonly TemplateBuilderComposeModule[]; readonly organization_id: string; readonly name: string;