From 10717572ac699844c4c562604dfad18de8f6073c Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Tue, 23 Jun 2026 11:36:07 -0400 Subject: [PATCH] feat: show template prerequisites in builder UI (#26523) Surface base template prerequisites to admins before they create a template in the Template Builder wizard. Today, template prerequisites (Docker socket setup, Kubernetes auth, AWS IAM policies) are only visible in the registry README after import. Admins hit opaque provisioner errors and have to hunt for docs. This change extracts the prerequisites from the README and serves them via the API so the frontend can display them inline. ## How it works Each base template README uses HTML comment markers (`` / ``) to delimit the prerequisites section. At boot time, the base catalog loader reads the README, extracts the content between markers via `strings.Index`, and caches both the full README and the prerequisites string. The prerequisites are served via a new `prerequisites` field on `GET /api/v2/templatebuilder/bases`. The full README is included in the composed template tar bundle and stored as the template version readme. ## Changes - Add `README.md` with prerequisite markers to `coderd/templatebuilder/bases/{docker,kubernetes,aws-linux}/` - New `ExtractPrerequisites()` in `prerequisites.go` using literal string matching - `bases.go`: load README at boot, fail loudly if missing, extract prerequisites - `compose.go`: include README in `ComposeResult` and tar bundle - `codersdk`: add `Prerequisites` field to `TemplateBuilderBase` - Handler: populate prerequisites in bases response, set readme on template version
Implementation notes - Prerequisites extraction uses `strings.Index` for exact literal marker matching; no regex or AST parser needed since we control the markers. - YAML frontmatter is deliberately retained in the stored README. The frontend `TemplateDocsPage` already strips it at render time via `front-matter`. - The prerequisite markers are HTML comments, invisible in rendered markdown. - The `RejectsMissingReadme` test enforces that every base template must include a README. - AWS Linux prerequisites span two H2 sections (`## Prerequisites` and `## Required permissions / policy`), which is why heading-based parsing was rejected in favor of explicit markers. *Generated with the assistance of an AI coding agent. Reviewed by @jeremyruppel.*
Relates to https://linear.app/codercom/issue/DEVEX-446 --- coderd/apidoc/docs.go | 3 + coderd/apidoc/swagger.json | 3 + coderd/templatebuilder/bases.go | 43 ++++++-- .../templatebuilder/bases/aws-linux/README.md | 98 +++++++++++++++++++ coderd/templatebuilder/bases/docker/README.md | 52 ++++++++++ .../bases/kubernetes/README.md | 42 ++++++++ coderd/templatebuilder/bases_internal_test.go | 37 +++++++ coderd/templatebuilder/bases_test.go | 43 ++++++++ coderd/templatebuilder/compose.go | 20 +++- coderd/templatebuilder/compose_test.go | 19 ++++ coderd/templatebuilder/prerequisites.go | 27 +++++ coderd/templatebuilder/prerequisites_test.go | 86 ++++++++++++++++ coderd/templatebuilder_handler.go | 15 +-- codersdk/templatebuilder.go | 13 +-- docs/reference/api/schemas.md | 19 ++-- docs/reference/api/templatebuilder.md | 1 + site/src/api/typesGenerated.ts | 1 + 17 files changed, 492 insertions(+), 30 deletions(-) create mode 100644 coderd/templatebuilder/bases/aws-linux/README.md create mode 100644 coderd/templatebuilder/bases/docker/README.md create mode 100644 coderd/templatebuilder/bases/kubernetes/README.md create mode 100644 coderd/templatebuilder/prerequisites.go create mode 100644 coderd/templatebuilder/prerequisites_test.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 006d15838e..4fc7a0c1bc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -23884,6 +23884,9 @@ const docTemplate = `{ "os": { "type": "string" }, + "prerequisites": { + "type": "string" + }, "variables": { "type": "array", "items": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5483aed895..994cb2402a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -21915,6 +21915,9 @@ "os": { "type": "string" }, + "prerequisites": { + "type": "string" + }, "variables": { "type": "array", "items": { diff --git a/coderd/templatebuilder/bases.go b/coderd/templatebuilder/bases.go index 5b5d79a60f..2d6a2406eb 100644 --- a/coderd/templatebuilder/bases.go +++ b/coderd/templatebuilder/bases.go @@ -52,9 +52,11 @@ type BaseDefaultContext struct { // parsedBase holds the result of loading and pre-parsing a single base // template directory. type parsedBase struct { - Manifest BaseManifest - Templates map[string]*template.Template - FS fs.FS + Manifest BaseManifest + Templates map[string]*template.Template + FS fs.FS + Readme string // full README.md content (including frontmatter) + Prerequisites string // content between prerequisite comment markers } var loadBases = sync.OnceValues(func() (map[string]*parsedBase, error) { @@ -114,10 +116,18 @@ func parseBasesFromFS(fsys fs.FS) (map[string]*parsedBase, error) { return nil, xerrors.Errorf("parse templates for base %q: %w", manifest.ID, err) } + readmeData, err := fs.ReadFile(baseFS, "README.md") + if err != nil { + return nil, xerrors.Errorf("read README.md for base %q: %w", manifest.ID, err) + } + readme := string(readmeData) + bases[manifest.ID] = &parsedBase{ - Manifest: manifest, - Templates: templates, - FS: baseFS, + Manifest: manifest, + Templates: templates, + FS: baseFS, + Readme: readme, + Prerequisites: ExtractPrerequisites(readme), } } @@ -233,3 +243,24 @@ func BaseTemplateFS(exampleID string) (fs.FS, error) { } return base.FS, nil } + +// BaseReadme returns the full README.md content for a base template. +// Returns an empty string if the base is unknown or has no README. +func BaseReadme(exampleID string) string { + bases, err := loadBases() + if err != nil || bases[exampleID] == nil { + return "" + } + return bases[exampleID].Readme +} + +// BasePrerequisites returns the prerequisites section extracted from +// the base template README. Returns an empty string if the base is +// unknown or has no prerequisites markers. +func BasePrerequisites(exampleID string) string { + bases, err := loadBases() + if err != nil || bases[exampleID] == nil { + return "" + } + return bases[exampleID].Prerequisites +} diff --git a/coderd/templatebuilder/bases/aws-linux/README.md b/coderd/templatebuilder/bases/aws-linux/README.md new file mode 100644 index 0000000000..3587221889 --- /dev/null +++ b/coderd/templatebuilder/bases/aws-linux/README.md @@ -0,0 +1,98 @@ +--- +display_name: AWS EC2 (Linux) +description: Provision AWS EC2 VMs as Coder workspaces +icon: ../../../site/static/icon/aws.svg +maintainer_github: coder +verified: true +tags: [vm, linux, aws, persistent-vm] +--- + +# Remote Development on AWS EC2 VMs (Linux) + +Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs/user-guides/workspace-management) with this example template. + + + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + + + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. diff --git a/coderd/templatebuilder/bases/docker/README.md b/coderd/templatebuilder/bases/docker/README.md new file mode 100644 index 0000000000..6398547ef5 --- /dev/null +++ b/coderd/templatebuilder/bases/docker/README.md @@ -0,0 +1,52 @@ +--- +display_name: Docker Containers +description: Provision Docker containers as Coder workspaces +icon: ../../../site/static/icon/docker.png +maintainer_github: coder +verified: true +tags: [docker, container] +--- + +# Remote Development on Docker Containers + +Provision Docker containers as [Coder workspaces](https://coder.com/docs/user-guides/workspace-management) with this example template. + + + + + +## Prerequisites + +### Infrastructure + +The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group: + +```sh +# Add coder user to Docker group +sudo adduser coder docker + +# Restart Coder server +sudo systemctl restart coder + +# Test Docker +sudo -u coder docker ps +``` + + + +## Architecture + +This template provisions the following resources: + +- Docker image (built by Docker socket and kept locally) +- Docker container pod (ephemeral) +- Docker volume (persistent on `/home/coder`) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/user-guides/workspace-dotfiles) their workspaces with dotfiles. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +### Editing the image + +Edit the `Dockerfile` and run `coder templates push` to update workspaces. diff --git a/coderd/templatebuilder/bases/kubernetes/README.md b/coderd/templatebuilder/bases/kubernetes/README.md new file mode 100644 index 0000000000..26e7e07e38 --- /dev/null +++ b/coderd/templatebuilder/bases/kubernetes/README.md @@ -0,0 +1,42 @@ +--- +display_name: Kubernetes (Deployment) +description: Provision Kubernetes Deployments as Coder workspaces +icon: ../../../site/static/icon/k8s.png +maintainer_github: coder +verified: true +tags: [kubernetes, container] +--- + +# Remote Development on Kubernetes Pods + +Provision Kubernetes Pods as [Coder workspaces](https://coder.com/docs/user-guides/workspace-management) with this example template. + + + + + +## Prerequisites + +### Infrastructure + +**Cluster**: This template requires an existing Kubernetes cluster + +**Container Image**: This template uses the [codercom/enterprise-base:ubuntu image](https://github.com/coder/enterprise-images/tree/main/images/base) with some dev tools preinstalled. To add additional tools, extend this image or build it yourself. + +### Authentication + +This template authenticates using a `~/.kube/config`, if present on the server, or via built-in authentication if the Coder provisioner is running on Kubernetes with an authorized ServiceAccount. To use another [authentication method](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication), edit the template. + + + +## Architecture + +This template provisions the following resources: + +- Kubernetes pod (ephemeral) +- Kubernetes persistent volume claim (persistent on `/home/coder`) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/user-guides/workspace-dotfiles) their workspaces with dotfiles. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. diff --git a/coderd/templatebuilder/bases_internal_test.go b/coderd/templatebuilder/bases_internal_test.go index 786dc3a523..19187dbf2e 100644 --- a/coderd/templatebuilder/bases_internal_test.go +++ b/coderd/templatebuilder/bases_internal_test.go @@ -27,6 +27,9 @@ func TestParseBasesFromFS(t *testing.T) { "bases/docker/main.tf.tmpl": &fstest.MapFile{ Data: []byte(`image = "{{ .ContainerImage }}"`), }, + "bases/docker/README.md": &fstest.MapFile{ + Data: []byte("# Docker\n"), + }, } bases, err := parseBasesFromFS(fsys) @@ -52,12 +55,18 @@ func TestParseBasesFromFS(t *testing.T) { "bases/alpha/main.tf.tmpl": &fstest.MapFile{ Data: []byte(`resource "alpha" {}`), }, + "bases/alpha/README.md": &fstest.MapFile{ + Data: []byte("# Alpha\n"), + }, "bases/beta/base.json": &fstest.MapFile{ Data: []byte(`{"id": "beta", "os": "linux"}`), }, "bases/beta/main.tf.tmpl": &fstest.MapFile{ Data: []byte(`resource "beta" {}`), }, + "bases/beta/README.md": &fstest.MapFile{ + Data: []byte("# Beta\n"), + }, } bases, err := parseBasesFromFS(fsys) @@ -93,6 +102,9 @@ func TestParseBasesFromFS(t *testing.T) { "bases/mybase/cloud-init/config.yaml.tftpl": &fstest.MapFile{ Data: []byte(`${some_terraform_var}`), }, + "bases/mybase/README.md": &fstest.MapFile{ + Data: []byte("# My Base\n"), + }, } bases, err := parseBasesFromFS(fsys) @@ -105,6 +117,22 @@ func TestParseBasesFromFS(t *testing.T) { require.NotContains(t, b.Templates, "cloud-init/config.yaml.tftpl") }) + t.Run("RejectsMissingReadme", func(t *testing.T) { + t.Parallel() + + fsys := fstest.MapFS{ + "bases/bad/base.json": &fstest.MapFile{ + Data: []byte(`{"id": "bad", "os": "linux"}`), + }, + "bases/bad/main.tf.tmpl": &fstest.MapFile{ + Data: []byte(`resource {}`), + }, + } + + _, err := parseBasesFromFS(fsys) + require.ErrorContains(t, err, "read README.md for base") + }) + t.Run("RejectsDirWithoutManifest", func(t *testing.T) { t.Parallel() @@ -136,9 +164,15 @@ func TestParseBasesFromFS(t *testing.T) { "bases/a/base.json": &fstest.MapFile{ Data: []byte(`{"id": "dupe", "os": "linux"}`), }, + "bases/a/README.md": &fstest.MapFile{ + Data: []byte("# A\n"), + }, "bases/b/base.json": &fstest.MapFile{ Data: []byte(`{"id": "dupe", "os": "linux"}`), }, + "bases/b/README.md": &fstest.MapFile{ + Data: []byte("# B\n"), + }, } _, err := parseBasesFromFS(fsys) @@ -207,6 +241,9 @@ func TestParseBasesFromFS(t *testing.T) { "bases/nospec/base.json": &fstest.MapFile{ Data: []byte(`{"id": "nospec"}`), }, + "bases/nospec/README.md": &fstest.MapFile{ + Data: []byte("# No Spec\n"), + }, } bases, err := parseBasesFromFS(fsys) diff --git a/coderd/templatebuilder/bases_test.go b/coderd/templatebuilder/bases_test.go index 70ed705cd3..c54a0db0c8 100644 --- a/coderd/templatebuilder/bases_test.go +++ b/coderd/templatebuilder/bases_test.go @@ -101,3 +101,46 @@ func TestBaseTemplateFS(t *testing.T) { require.Contains(t, err.Error(), "unknown base template") }) } + +func TestBaseReadme(t *testing.T) { + t.Parallel() + + t.Run("KnownBasesHaveReadme", func(t *testing.T) { + t.Parallel() + for _, id := range templatebuilder.BaseTemplateIDs() { + readme := templatebuilder.BaseReadme(id) + require.NotEmpty(t, readme, "base %q should have a README", id) + } + }) + + t.Run("UnknownReturnsEmpty", func(t *testing.T) { + t.Parallel() + require.Empty(t, templatebuilder.BaseReadme("nonexistent")) + }) +} + +func TestBasePrerequisites(t *testing.T) { + t.Parallel() + + t.Run("KnownBasesHavePrerequisites", func(t *testing.T) { + t.Parallel() + for _, id := range templatebuilder.BaseTemplateIDs() { + prereqs := templatebuilder.BasePrerequisites(id) + require.NotEmpty(t, prereqs, "base %q should have prerequisites", id) + require.Contains(t, prereqs, "## Prerequisites", + "base %q prerequisites should contain the heading", id) + } + }) + + t.Run("AWSLinuxIncludesPermissions", func(t *testing.T) { + t.Parallel() + prereqs := templatebuilder.BasePrerequisites("aws-linux") + require.Contains(t, prereqs, "## Required permissions / policy", + "AWS Linux prerequisites should include the permissions section") + }) + + t.Run("UnknownReturnsEmpty", func(t *testing.T) { + t.Parallel() + require.Empty(t, templatebuilder.BasePrerequisites("nonexistent")) + }) +} diff --git a/coderd/templatebuilder/compose.go b/coderd/templatebuilder/compose.go index 9a400bd00e..dc7bb0853d 100644 --- a/coderd/templatebuilder/compose.go +++ b/coderd/templatebuilder/compose.go @@ -47,6 +47,9 @@ type ComposeResult struct { // ModulesTF is the concatenated rendered module blocks. Empty when // no modules are selected. ModulesTF []byte + // Readme is the full README.md content from the base template. + // Empty when the base has no README. + Readme []byte } // Compose renders a base template and selected modules into Terraform @@ -59,7 +62,10 @@ func Compose(req ComposeRequest) (*ComposeResult, error) { } if len(req.Modules) == 0 { - return &ComposeResult{MainTF: formatHCL(mainTF)}, nil + return &ComposeResult{ + MainTF: formatHCL(mainTF), + Readme: []byte(BaseReadme(req.BaseTemplateID)), + }, nil } agentName, err := ExtractAgentResourceName(mainTF) @@ -82,10 +88,12 @@ func Compose(req ComposeRequest) (*ComposeResult, error) { return nil, err } - return &ComposeResult{ + result := &ComposeResult{ MainTF: formatHCL(mainTF), ModulesTF: formatHCL(modulesTF), - }, nil + Readme: []byte(BaseReadme(req.BaseTemplateID)), + } + return result, nil } // formatHCL applies canonical HCL formatting to src. If src is not valid @@ -448,6 +456,12 @@ func BundleTar(result *ComposeResult) ([]byte, error) { } } + if len(result.Readme) > 0 { + if err := writeTarFile(tw, "README.md", result.Readme); err != nil { + return nil, xerrors.Errorf("write README.md to tar: %w", err) + } + } + if err := tw.Close(); err != nil { return nil, xerrors.Errorf("close tar writer: %w", err) } diff --git a/coderd/templatebuilder/compose_test.go b/coderd/templatebuilder/compose_test.go index 9eae66e326..0bf1eb851a 100644 --- a/coderd/templatebuilder/compose_test.go +++ b/coderd/templatebuilder/compose_test.go @@ -25,6 +25,7 @@ func TestCompose(t *testing.T) { require.NotEmpty(t, result.MainTF) require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`) require.Empty(t, result.ModulesTF) + require.NotEmpty(t, result.Readme, "compose should include base README") }) t.Run("BaseWithModuleAndVariableOverride", func(t *testing.T) { @@ -263,6 +264,7 @@ func TestBundleTar(t *testing.T) { files := extractTar(t, data) require.Contains(t, files, "main.tf") require.NotContains(t, files, "modules.tf") + require.NotContains(t, files, "README.md") require.Equal(t, "resource {}", files["main.tf"]) }) @@ -278,10 +280,26 @@ func TestBundleTar(t *testing.T) { files := extractTar(t, data) require.Contains(t, files, "main.tf") require.Contains(t, files, "modules.tf") + require.NotContains(t, files, "README.md") require.Equal(t, "resource {}", files["main.tf"]) require.Equal(t, "module {}", files["modules.tf"]) }) + t.Run("IncludesReadme", func(t *testing.T) { + t.Parallel() + result := &templatebuilder.ComposeResult{ + MainTF: []byte("resource {}"), + Readme: []byte("# My Template\n"), + } + data, err := templatebuilder.BundleTar(result) + require.NoError(t, err) + + files := extractTar(t, data) + require.Contains(t, files, "main.tf") + require.Contains(t, files, "README.md") + require.Equal(t, "# My Template\n", files["README.md"]) + }) + t.Run("RoundTrip", func(t *testing.T) { t.Parallel() result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{ @@ -299,6 +317,7 @@ func TestBundleTar(t *testing.T) { files := extractTar(t, data) require.Equal(t, string(result.MainTF), files["main.tf"]) require.Equal(t, string(result.ModulesTF), files["modules.tf"]) + require.Equal(t, string(result.Readme), files["README.md"]) }) t.Run("ReproducibleArchive", func(t *testing.T) { diff --git a/coderd/templatebuilder/prerequisites.go b/coderd/templatebuilder/prerequisites.go new file mode 100644 index 0000000000..52658d02bc --- /dev/null +++ b/coderd/templatebuilder/prerequisites.go @@ -0,0 +1,27 @@ +package templatebuilder + +import "strings" + +const ( + // prerequisitesStartMarker delimits the beginning of the prerequisites + // section inside a base template README.md. + prerequisitesStartMarker = "" + // prerequisitesEndMarker delimits the end of the prerequisites section. + prerequisitesEndMarker = "" +) + +// ExtractPrerequisites returns the content between the prerequisites +// comment markers in a README body. Returns an empty string when either +// marker is absent. +func ExtractPrerequisites(readme string) string { + startIdx := strings.Index(readme, prerequisitesStartMarker) + if startIdx < 0 { + return "" + } + after := readme[startIdx+len(prerequisitesStartMarker):] + endIdx := strings.Index(after, prerequisitesEndMarker) + if endIdx < 0 { + return "" + } + return strings.TrimSpace(after[:endIdx]) +} diff --git a/coderd/templatebuilder/prerequisites_test.go b/coderd/templatebuilder/prerequisites_test.go new file mode 100644 index 0000000000..65c99d5a0c --- /dev/null +++ b/coderd/templatebuilder/prerequisites_test.go @@ -0,0 +1,86 @@ +package templatebuilder_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/templatebuilder" +) + +func TestExtractPrerequisites(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + readme string + expected string + }{ + { + name: "BothMarkers", + readme: "# Title\n\n" + + "\n" + + "## Prerequisites\n\n" + + "Install Docker.\n" + + "\n\n" + + "## Architecture\n", + expected: "## Prerequisites\n\nInstall Docker.", + }, + { + name: "MultipleH2Sections", + readme: "# Title\n\n" + + "\n" + + "## Prerequisites\n\n" + + "Auth stuff.\n\n" + + "## Required permissions\n\n" + + "Policy JSON.\n" + + "\n\n" + + "## Architecture\n", + expected: "## Prerequisites\n\nAuth stuff.\n\n## Required permissions\n\nPolicy JSON.", + }, + { + name: "NoMarkers", + readme: "# Title\n\n## Prerequisites\n\nSome content.\n\n## Architecture\n", + expected: "", + }, + { + name: "StartMarkerOnly", + readme: "# Title\n\n" + + "\n" + + "## Prerequisites\n\nContent.\n", + expected: "", + }, + { + name: "EndMarkerOnly", + readme: "# Title\n\n" + + "## Prerequisites\n\nContent.\n" + + "\n", + expected: "", + }, + { + name: "EmptyBetweenMarkers", + readme: "\n" + + "\n", + expected: "", + }, + { + name: "NestedH3Headings", + readme: "\n" + + "## Prerequisites\n\n" + + "### Infrastructure\n\n" + + "Docker socket required.\n\n" + + "### Authentication\n\n" + + "Use kubeconfig.\n" + + "\n", + expected: "## Prerequisites\n\n### Infrastructure\n\nDocker socket required.\n\n### Authentication\n\nUse kubeconfig.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := templatebuilder.ExtractPrerequisites(tt.readme) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/coderd/templatebuilder_handler.go b/coderd/templatebuilder_handler.go index 3c18e19bee..5d4e522ee4 100644 --- a/coderd/templatebuilder_handler.go +++ b/coderd/templatebuilder_handler.go @@ -74,12 +74,13 @@ func (api *API) templateBuilderBases(rw http.ResponseWriter, r *http.Request) { } 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, + ID: ex.ID, + Name: ex.Name, + Description: ex.Description, + Icon: ex.Icon, + OS: string(templatebuilder.BaseTemplateOS(id)), + Variables: vars, + Prerequisites: templatebuilder.BasePrerequisites(id), }) } @@ -422,7 +423,7 @@ func (api *API) templateBuilderCreateTemplate(rw http.ResponseWriter, r *http.Re UpdatedAt: dbtime.Now(), Name: versionName, Message: "", - Readme: "", + Readme: string(result.Readme), JobID: provisionerJob.ID, CreatedBy: apiKey.UserID, SourceExampleID: sql.NullString{}, diff --git a/codersdk/templatebuilder.go b/codersdk/templatebuilder.go index 32406752b6..0bcb36ca05 100644 --- a/codersdk/templatebuilder.go +++ b/codersdk/templatebuilder.go @@ -52,12 +52,13 @@ 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"` - Variables []TemplateBuilderModuleVariable `json:"variables"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + OS string `json:"os"` + Variables []TemplateBuilderModuleVariable `json:"variables"` + Prerequisites string `json:"prerequisites"` } // TemplateBuilderBasesResponse is the response body for listing template builder bases. diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 12c7d03bab..054c8bd71c 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -11924,6 +11924,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "id": "string", "name": "string", "os": "string", + "prerequisites": "string", "variables": [ { "default": [ @@ -11941,14 +11942,15 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ### Properties -| 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 | | | +| Name | Type | Required | Restrictions | Description | +|-----------------|-------------------------------------------------------------------------------------------|----------|--------------|-------------| +| `description` | string | false | | | +| `icon` | string | false | | | +| `id` | string | false | | | +| `name` | string | false | | | +| `os` | string | false | | | +| `prerequisites` | string | false | | | +| `variables` | array of [codersdk.TemplateBuilderModuleVariable](#codersdktemplatebuildermodulevariable) | false | | | ## codersdk.TemplateBuilderBasesResponse @@ -11961,6 +11963,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "id": "string", "name": "string", "os": "string", + "prerequisites": "string", "variables": [ { "default": [ diff --git a/docs/reference/api/templatebuilder.md b/docs/reference/api/templatebuilder.md index 05ba6cc720..f1333ca897 100644 --- a/docs/reference/api/templatebuilder.md +++ b/docs/reference/api/templatebuilder.md @@ -26,6 +26,7 @@ curl -X GET http://coder-server:8080/api/v2/templatebuilder/bases \ "id": "string", "name": "string", "os": "string", + "prerequisites": "string", "variables": [ { "default": [ diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 03f9b3e27e..72a042a884 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -8282,6 +8282,7 @@ export interface TemplateBuilderBase { readonly icon: string; readonly os: string; readonly variables: readonly TemplateBuilderModuleVariable[]; + readonly prerequisites: string; } // From codersdk/templatebuilder.go