From a797a494ef04367d03823ef42c70577befab0805 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Mar 2026 15:34:03 +0200 Subject: [PATCH] feat: add starter template option and Coder Desktop URLs to scripts/develop (#23149) - Add `--starter-template` option and properly create starter template with name and icon - Add Coder Desktop URLs to listening banner - Makefile tweak to avoid rebuilding `scripts/develop` every time Go code changes --- Makefile | 17 ++--- scripts/develop/main.go | 151 ++++++++++++++++++++++++---------------- 2 files changed, 98 insertions(+), 70 deletions(-) diff --git a/Makefile b/Makefile index bc578ed90b..ca4a4ed4a6 100644 --- a/Makefile +++ b/Makefile @@ -136,18 +136,10 @@ endif # the search path so that these exclusions match. FIND_EXCLUSIONS= \ -not \( \( -path '*/.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path '*/node_modules/*' -o -path '*/out/*' -o -path './coderd/apidoc/*' -o -path '*/.next/*' -o -path '*/.terraform/*' -o -path './_gen/*' \) -prune \) + # Source files used for make targets, evaluated on use. GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -name '*_test.go') -# Same as GO_SRC_FILES but excluding certain files that have problematic -# Makefile dependencies (e.g. pnpm). -MOST_GO_SRC_FILES := $(shell \ - find . \ - $(FIND_EXCLUSIONS) \ - -type f \ - -name '*.go' \ - -not -name '*_test.go' \ - -not -wholename './agent/agentcontainers/dcspec/dcspec_gen.go' \ -) + # All the shell files in the repo, excluding ignored files. SHELL_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.sh') @@ -514,7 +506,10 @@ install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT) cp "$<" "$$output_file" .PHONY: install -build/.bin/develop: go.mod go.sum $(GO_SRC_FILES) +# Only wildcard the go files in the develop directory to avoid rebuilds +# when project files are changd. Technically changes to some imports may +# not be detected, but it's unlikely to cause any issues. +build/.bin/develop: go.mod go.sum $(wildcard scripts/develop/*.go) CGO_ENABLED=0 go build -o $@ ./scripts/develop BOLD := $(shell tput bold 2>/dev/null) diff --git a/scripts/develop/main.go b/scripts/develop/main.go index 0ca72f5b6f..d639e2a842 100644 --- a/scripts/develop/main.go +++ b/scripts/develop/main.go @@ -31,17 +31,19 @@ import ( "cdr.dev/slog/v3/sloggers/sloghuman" "github.com/coder/coder/v2/cli" "github.com/coder/coder/v2/cli/config" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" ) const ( - defaultAPIPort = "3000" - defaultWebPort = "8080" - defaultProxyPort = "3010" - defaultPassword = "SomeSecurePassword!" - healthTimeout = 60 * time.Second - shutdownTimeout = 15 * time.Second + defaultAPIPort = "3000" + defaultWebPort = "8080" + defaultProxyPort = "3010" + defaultPassword = "SomeSecurePassword!" + defaultStarterTemplate = "docker" + healthTimeout = 60 * time.Second + shutdownTimeout = 15 * time.Second ) func main() { @@ -106,6 +108,13 @@ func main() { Description: "Run under Delve debugger.", Value: serpent.BoolOf(&cfg.debug), }, + { + Flag: "starter-template", + Env: "CODER_DEV_STARTER_TEMPLATE", + Default: defaultStarterTemplate, + Description: "Starter template to create (empty to skip).", + Value: serpent.StringOf(&cfg.starterTemplate), + }, }, Handler: func(inv *serpent.Invocation) error { cfg.serverExtraArgs = inv.Args @@ -129,19 +138,20 @@ func main() { } type devConfig struct { - apiPort int64 - webPort int64 - proxyPort int64 - agpl bool - accessURL string - password string - useProxy bool - multiOrg bool - debug bool - projectRoot string - binaryPath string - configDir string - childEnv []string + apiPort int64 + webPort int64 + proxyPort int64 + agpl bool + accessURL string + password string + useProxy bool + multiOrg bool + debug bool + starterTemplate string + projectRoot string + binaryPath string + configDir string + childEnv []string // Extra args after flags forwarded to "coder server". serverExtraArgs []string } @@ -366,8 +376,10 @@ func develop(ctx context.Context, logger slog.Logger, cfg *devConfig) error { } } - if err := setupDockerTemplate(ctx, logger, cfg, client); err != nil { - logger.Warn(ctx, "docker template setup failed, continuing", slog.Error(err)) + if cfg.starterTemplate != "" { + if err := setupStarterTemplate(ctx, logger, cfg, client); err != nil { + logger.Warn(ctx, "starter template setup failed, continuing", slog.Error(err)) + } } if cfg.useProxy { @@ -688,32 +700,49 @@ func setupWorkspaceProxy(ctx context.Context, cfg *devConfig, client *codersdk.C return group.Start("proxy", cmd) } -// setupDockerTemplate creates a Docker template from the embedded -// starter example when Docker is available and the template does -// not already exist. Uses the SDK's ExampleID field so the server -// resolves the template archive internally, no CLI shelling needed. -func setupDockerTemplate(ctx context.Context, logger slog.Logger, cfg *devConfig, client *codersdk.Client) error { - // Check if Docker is available. - if err := exec.CommandContext(ctx, "docker", "info").Run(); err != nil { - logger.Debug(ctx, "docker not available, skipping template setup") - return nil +// setupStarterTemplate creates a template from a starter example. +// For starters tagged with "docker", it checks Docker availability +// and resolves the Docker host for template variables. +func setupStarterTemplate(ctx context.Context, logger slog.Logger, cfg *devConfig, client *codersdk.Client) error { + templateID := cfg.starterTemplate + + // Fetch starter template metadata from the running coderd. + examples, err := client.StarterTemplates(ctx) + if err != nil { + return xerrors.Errorf("fetch starter templates failed: %w", err) + } + example, ok := slice.Find(examples, func(e codersdk.TemplateExample) bool { + return e.ID == templateID + }) + if !ok { + return xerrors.Errorf("starter template %q not found", templateID) } - // Resolve Docker host for template variables. - dockerHost := "" - if out, err := exec.CommandContext(ctx, "docker", "context", "inspect", - "--format", "{{ .Endpoints.docker.Host }}").Output(); err == nil { - dockerHost = strings.TrimSpace(string(out)) + // Docker-specific: check availability and resolve host. + var userVars []codersdk.VariableValue + if slices.Contains(example.Tags, "docker") { + if err := exec.CommandContext(ctx, "docker", "info").Run(); err != nil { + logger.Debug(ctx, "docker not available, skipping template setup") + return nil + } + dockerHost := "" + if out, err := exec.CommandContext(ctx, "docker", "context", "inspect", + "--format", "{{ .Endpoints.docker.Host }}").Output(); err == nil { + dockerHost = strings.TrimSpace(string(out)) + } + userVars = []codersdk.VariableValue{ + {Name: "docker_arch", Value: runtime.GOARCH}, + {Name: "docker_host", Value: dockerHost}, + } } - if err := createTemplateInOrg(ctx, logger, client, codersdk.DefaultOrganization, dockerHost); err != nil { + if err := createTemplateInOrg(ctx, logger, client, codersdk.DefaultOrganization, example, userVars); err != nil { return err } if cfg.multiOrg { - if err := createTemplateInOrg(ctx, logger, client, "second-organization", dockerHost); err != nil { - logger.Warn(ctx, "failed to create docker template in second org", - slog.Error(err)) + if err := createTemplateInOrg(ctx, logger, client, "second-organization", example, userVars); err != nil { + logger.Warn(ctx, "failed to create starter template in second org", slog.Error(err)) } } @@ -742,30 +771,27 @@ func waitForVersion(ctx context.Context, client *codersdk.Client, id uuid.UUID) }) } -// createTemplateInOrg ensures the "docker" template exists in the -// given org, creating it from the embedded example if needed. -func createTemplateInOrg(ctx context.Context, logger slog.Logger, client *codersdk.Client, orgName string, dockerHost string) error { +// createTemplateInOrg ensures a starter template exists in the +// given org, creating it from the example if needed. +func createTemplateInOrg(ctx context.Context, logger slog.Logger, client *codersdk.Client, orgName string, example codersdk.TemplateExample, userVars []codersdk.VariableValue) error { org, err := client.OrganizationByName(ctx, orgName) if err != nil { - return xerrors.Errorf("looking up org %q: %w", orgName, err) + return xerrors.Errorf("look up org %q failed: %w", orgName, err) } - if _, err := client.TemplateByName(ctx, org.ID, "docker"); err == nil { - logger.Debug(ctx, "docker template already exists", - slog.F("org", orgName)) + if _, err := client.TemplateByName(ctx, org.ID, example.ID); err == nil { + logger.Debug(ctx, "template already exists, skipping creation", slog.F("template", example.ID), slog.F("org", orgName)) return nil } + version, err := client.CreateTemplateVersion(ctx, org.ID, codersdk.CreateTemplateVersionRequest{ - StorageMethod: codersdk.ProvisionerStorageMethodFile, - ExampleID: "docker", - Provisioner: codersdk.ProvisionerTypeTerraform, - UserVariableValues: []codersdk.VariableValue{ - {Name: "docker_arch", Value: runtime.GOARCH}, - {Name: "docker_host", Value: dockerHost}, - }, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + ExampleID: example.ID, + Provisioner: codersdk.ProvisionerTypeTerraform, + UserVariableValues: userVars, }) if err != nil { - return xerrors.Errorf("creating version: %w", err) + return xerrors.Errorf("create template version failed: %w", err) } version, err = waitForVersion(ctx, client, version.ID) if err != nil { @@ -773,14 +799,16 @@ func createTemplateInOrg(ctx context.Context, logger slog.Logger, client *coders } _, err = client.CreateTemplate(ctx, org.ID, codersdk.CreateTemplateRequest{ - Name: "docker", - VersionID: version.ID, + Name: example.ID, + DisplayName: example.Name, + Description: example.Description, + Icon: example.Icon, + VersionID: version.ID, }) if err != nil { - return xerrors.Errorf("creating template: %w", err) + return xerrors.Errorf("create template failed: %w", err) } - logger.Info(ctx, "docker template created in org", - slog.F("org", orgName)) + logger.Info(ctx, "template created in org", slog.F("template", example.ID), slog.F("org", orgName)) return nil } @@ -802,6 +830,11 @@ func printBanner(ctx context.Context, logger slog.Logger, cfg *devConfig) { } } } + if os.Getenv("CODER") == "true" { + // Inside a workspace, add Coder Desktop entry. + ifaces = append(ifaces, fmt.Sprintf("%s.%s.me.coder", os.Getenv("CODER_WORKSPACE_AGENT_NAME"), os.Getenv("CODER_WORKSPACE_NAME"))) + ifaces = append(ifaces, fmt.Sprintf("%s.%s.%s.coder", os.Getenv("CODER_WORKSPACE_AGENT_NAME"), os.Getenv("CODER_WORKSPACE_NAME"), os.Getenv("CODER_WORKSPACE_OWNER_NAME"))) + } var b strings.Builder w := 64 line := func(content string) {