From b2db789b2d1df62d6ce42f18f48e635a86aac2c8 Mon Sep 17 00:00:00 2001 From: Olof Mattsson <14146601+omattsson@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:49:50 +0200 Subject: [PATCH] feat(definition,template): add and remove charts, check-upgrade, upgrade, link-template (#157, #158) - definition add-chart and remove-chart (devops or admin). remove-chart takes a chart name or ID and asks for confirmation unless --yes. - definition check-upgrade: the version change and a CHART/CHANGE table. Without an upgrade the server sends no reason; the command reads the template link of the definition. A chart with differences in chart_diffs (for example another chart version) shows as modified. -q prints yes or no. - definition upgrade: shows the check on stderr, warns that the template values replace the definition values, and asks unless --yes. - definition link-template --template [--version] or --unlink (server v0.11.0 or later; a missing route gives an upgrade hint). - template add-chart and remove-chart change the working copy and write a note to publish with a new --version. - Flag names follow update-chart (--name, --repository-url, --chart-path, --chart-version, --file, --locked-file, --required, --deploy-order). - Refresh the vendored swagger.json (the contract test checks the new template-link path). Closes #157 Closes #158 Co-Authored-By: Claude Opus 5.5 --- .github/copilot-instructions.md | 4 +- CLAUDE.md | 7 +- README.md | 20 +- cli/cmd/chart_changes.go | 597 ++++++++++++ cli/cmd/chart_changes_test.go | 450 +++++++++ cli/pkg/client/client.go | 61 ++ cli/pkg/client/definition_charts_test.go | 68 ++ cli/pkg/types/types.go | 33 + cli/test/contract/testdata/swagger.json | 1058 +++++++++++++++++----- 9 files changed, 2058 insertions(+), 240 deletions(-) create mode 100644 cli/cmd/chart_changes.go create mode 100644 cli/cmd/chart_changes_test.go create mode 100644 cli/pkg/client/definition_charts_test.go diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2e5dfe5..e5fddde 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,8 +23,8 @@ stackctl is a Go CLI tool (Cobra) for managing Kubernetes stack deployments. It | `login`/`logout`/`whoami` | top-level; supports password + OIDC loopback flow | login.go | | `auth` | register | login.go | | `stack` | list, get, create, update, deploy, stop, clean, delete, status, logs, clone, extend, values, compare, history, rollback, history-values, watch, preview, pods, recent, follow, unfollow, action (list, run, log) | stack.go; preview/pods/recent: stack_inspect.go; follow/unfollow: stack_followers.go; action: stack_action.go; TTL and stop-time helpers: ttl.go | -| `template` | list, get, create, update, delete, clone, instantiate, quick-deploy, publish, unpublish, update-chart, versions (list, get, diff) | template.go | -| `definition` | list, get, create, update, delete, export, import, update-chart | definition.go | +| `template` | list, get, create, update, delete, clone, instantiate, quick-deploy, publish, unpublish, update-chart, add-chart, remove-chart, versions (list, get, diff) | template.go; add-chart/remove-chart: chart_changes.go | +| `definition` | list, get, create, update, delete, export, import, update-chart, add-chart, remove-chart, check-upgrade, upgrade, link-template | definition.go; add-chart/remove-chart/check-upgrade/upgrade/link-template: chart_changes.go | | `override` | list, get, set, unset, delete, branch (list/set/delete), quota (get/set/delete) | override.go | | `bulk` | deploy, stop, clean, delete, template (publish, unpublish, delete) | bulk.go | | `git` | providers, branches, validate | git.go | diff --git a/CLAUDE.md b/CLAUDE.md index c571f78..38fb1e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,7 @@ cli/ # Go module root (github.com/omattsson/stackctl/cl server.go # server info (ping, ui-config; public routes, no credentials) template.go # template list/get/create/update/clone/publish/unpublish/versions/instantiate/quick-deploy/delete definition.go # definition list/get/create/update/delete/export/import/update-chart + chart_changes.go # definition add-chart/remove-chart/check-upgrade/upgrade/link-template, template add-chart/remove-chart override.go # override list/get/set/unset/delete, branch overrides, quota overrides (quota set merges with GET, --replace) bulk.go # bulk deploy/stop/clean/delete, bulk template publish/unpublish/delete (--ids flag or positional args) git.go # git providers/branches/validate @@ -187,8 +188,12 @@ All API calls go to `/api/v1/*`. Key route groups: - `/api/v1/admin/orphaned-namespaces` — `orphaned list [--details]`; `DELETE /:ns?confirm=` is needed for a namespace without the label managed-by=k8s-stack-manager (409 without it; `orphaned delete --confirm`). `test/contract/path_contract_test.go` checks every `/api/v1` literal of `pkg/client` and `cmd` against the vendored swagger - `/api/v1/notifications/preferences` — `notification prefs set` warns for an event type not in `notificationEventTypes` (`notifier.AllEventTypes()` of k8s-stack-manager); v0.8.0+ servers return 400 - `/api/v1/stack-definitions` — CRUD + export/import + - `POST /:id/charts` and `DELETE /:id/charts/:chartId` (devops or admin; `definition add-chart`, `remove-chart`). A linked definition cannot lose a chart that the template requires (409 "Cannot delete required chart: X"). + - `GET /:id/check-upgrade` sends only `{"upgrade_available": false}` when there is no upgrade (no source template, unpublished template, latest version). `printUpgradeCheck` reads the definition (`source_template_id`, `source_template_version`) for the reason. The server marks a chart `modified` only for other default values or another repository URL; the table also marks a chart with `has_differences` in `chart_diffs` as modified. + - `POST /:id/upgrade` (owner, devops or admin) replaces the default values of the template charts. `definition upgrade` shows the check on stderr and confirms unless `--yes`. 409 "Definition is already at the latest version" or "Template has no published version". + - `PUT /:id/template-link` `{"template_id","version"}` (k8s-stack-manager v0.11.0+, devops or admin; empty `template_id` removes the link). A plain 404 (no route) maps to the v0.11.0 upgrade hint (`linkTemplateError`). - `/api/v1/templates` — list/get/instantiate/quick-deploy/publish/versions - - Draft and release (k8s-stack-manager v0.6.0+): `PUT /:id` and the template chart routes change only the working copy. Users, quick deploy, instantiate and definition upgrades get the latest release. After `template update-chart`, run `template publish --version ` so users get the change. + - Draft and release (k8s-stack-manager v0.6.0+): `PUT /:id` and the template chart routes change only the working copy. `POST /:id/charts` and `DELETE /:id/charts/:chartId` (`template add-chart`, `remove-chart`) write a note to stderr: publish with a new `--version` (the working-copy version already exists after a change). Users, quick deploy, instantiate and definition upgrades get the latest release. After `template update-chart`, run `template publish --version ` so users get the change. - `POST /:id/publish` takes an optional body `{"version", "change_summary"}`; `template publish` sends it only when `--version` or `--change-summary` is set. Without a version the server uses the working-copy version. 409 "Version x already exists" and 400 "Version is required to publish" map to messages with the next command. 200 with `snapshot_created: false` prints "No changes since version X; no new version created". With `--version` or `--change-summary`, the command first does GET `/:id`; when `has_unpublished_changes` is absent (older server, which ignores the body) it fails with the v0.6.0 upgrade hint before the publish. - `PUT /:id`: v0.6.0+ changes only the fields sent (`PatchTemplateRequest`, pointer fields; `""` clears); older servers replace name/description/category/version/default_branch. `template update` does GET first: with `has_unpublished_changes` present it sends only the changed fields, else the full record (`UpdateTemplateRequest`). `--version` sets the working-copy version; `--description ""` clears. - Name resolution (`resolveTemplateID`, used by `template publish` and bulk template commands): sends `?name=` (v0.5.0 ignores it), pages through the result with `pageSize=100`, and matches the exact name on the client. 0 → not found; more than 1 → "multiple templates named X: ". diff --git a/README.md b/README.md index 23e1cce..e40f529 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,11 @@ stackctl template update-chart 8c9d0e1f-2a3b-4c5d-9e6f-7a8b9c0d1e2f 3f2b8c1e-5a4 stackctl template update-chart 8c9d0e1f-2a3b-4c5d-9e6f-7a8b9c0d1e2f 3f2b8c1e-5a4d-4e6f-9a7b-1c2d3e4f5a6b --file values.yaml --locked-file locked.yaml stackctl template update-chart 8c9d0e1f-2a3b-4c5d-9e6f-7a8b9c0d1e2f 3f2b8c1e-5a4d-4e6f-9a7b-1c2d3e4f5a6b --build-pipeline-id 42 +# Add or remove a chart of the working copy (template name or ID; chart name or ID) +stackctl template add-chart my-template --name worker --repository-url oci://registry.example.com/helm --chart-version 1.2.0 +stackctl template add-chart my-template --name api --chart-path /charts/api --required --locked-file locked.yaml +stackctl template remove-chart my-template worker + # Release the working copy to users (name or ID) stackctl template publish my-template --version 1.2.0 --change-summary "app-core chart 0.3.7" @@ -365,7 +370,7 @@ Most template commands need the template ID. `template publish` accepts a name o Templates use a draft-and-release model: -- `template update` and `template update-chart` change only the working copy (draft). +- `template update`, `template update-chart`, `template add-chart` and `template remove-chart` change only the working copy (draft). `add-chart` and `remove-chart` write a note to stderr: run `template publish` so users get the change. - Users get the latest released version. Quick deploy, instantiate and definition upgrades use it. - After `template update-chart`, run `stackctl template publish --version ` so users get the change. - Each version can be released only once. Publish without changes creates no new version. @@ -397,6 +402,18 @@ stackctl definition update-chart example-dev my-chart --deploy-order 6 stackctl definition update-chart example-dev my-chart --file values.yaml stackctl definition update-chart example-dev my-chart --build-pipeline-id 42 +# Add or remove a chart (devops or admin; remove-chart asks for confirmation unless --yes) +stackctl definition add-chart example-dev --name worker --repository-url oci://registry.example.com/helm --chart-version 1.2.0 +stackctl definition remove-chart example-dev worker + +# Upgrade to the latest published version of the source template +stackctl definition check-upgrade example-dev +stackctl definition upgrade example-dev # shows the changes, then asks for confirmation (--yes skips it) + +# Link a definition to a published template version, or remove the link (devops or admin) +stackctl definition link-template example-dev --template web-stack --version 1.2.0 +stackctl definition link-template example-dev --unlink + # Delete stackctl definition delete example-dev @@ -598,6 +615,7 @@ These groups have no section above. Run `stackctl --help` for the flags. | `stack extend --minutes`, template draft and release, `template publish --version` | v0.6.0 or later | | Owner, cluster and definition names in output, audit action and entity type values, paged `--mine` and name queries, quota override limit check, quota warning on `stack clone` | v0.7.0 or later | | `--follow` and `stack watch` with an API key, `bulk delete` with clean first, `orphaned delete --confirm`, `user set-role` | v0.8.0 or later | +| `definition link-template` | v0.11.0 or later | | `--stop-at`, `stack extend --until`, the TTL limit of `server info` and of the local check (an older server ignores `--stop-at` and `--until`: stackctl shows the result and exits with code 1; it uses 30 days as the limit) | v0.12.0 or later | ## Output Formats diff --git a/cli/cmd/chart_changes.go b/cli/cmd/chart_changes.go new file mode 100644 index 0000000..4149e59 --- /dev/null +++ b/cli/cmd/chart_changes.go @@ -0,0 +1,597 @@ +package cmd + +import ( + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/omattsson/stackctl/cli/pkg/client" + "github.com/omattsson/stackctl/cli/pkg/output" + "github.com/omattsson/stackctl/cli/pkg/types" + "github.com/spf13/cobra" +) + +// Commands that add or remove the charts of a definition or a template, +// and the template upgrade and link of a definition. + +// addChartFlags registers the flags of "add-chart". withTemplate adds the +// flags of a template chart (--locked-file, --required). +func addChartFlags(cmd *cobra.Command, withTemplate bool) { + cmd.Flags().String("name", "", "Chart name (required)") + cmd.Flags().String("repository-url", "", "Helm chart repository URL (e.g. oci://acr.example.com/helm)") + cmd.Flags().String("chart-path", "", "Chart path (e.g. /charts/app-core)") + cmd.Flags().String("chart-version", "", "Chart version") + cmd.Flags().String("source-repo-url", "", "Git repository URL for branch listing") + cmd.Flags().String("build-pipeline-id", "", "CI pipeline ID that builds the chart's image (used by a pre-deploy CI gate)") + cmd.Flags().Int("deploy-order", 0, "Deploy order (0+)") + cmd.Flags().String("file", "", "File containing default values") + if withTemplate { + cmd.Flags().String("locked-file", "", "File containing locked values (always win over instance overrides)") + cmd.Flags().Bool("required", false, "The chart is required: a definition from the template must keep it") + } + _ = cmd.MarkFlagRequired("name") +} + +// chartFields are the values of the add-chart flags. +type chartFields struct { + name, repositoryURL, chartPath, chartVersion, sourceRepoURL, buildPipelineID string + defaultValues, lockedValues string + deployOrder int + required bool +} + +// readChartFlags reads and checks the add-chart flags before any request. +func readChartFlags(cmd *cobra.Command) (chartFields, error) { + var f chartFields + f.name, _ = cmd.Flags().GetString("name") + f.repositoryURL, _ = cmd.Flags().GetString("repository-url") + f.chartPath, _ = cmd.Flags().GetString("chart-path") + f.chartVersion, _ = cmd.Flags().GetString("chart-version") + f.sourceRepoURL, _ = cmd.Flags().GetString("source-repo-url") + f.buildPipelineID, _ = cmd.Flags().GetString("build-pipeline-id") + f.deployOrder, _ = cmd.Flags().GetInt("deploy-order") + if strings.TrimSpace(f.name) == "" { + return f, fmt.Errorf("--name is required") + } + if f.deployOrder < 0 { + return f, fmt.Errorf("--deploy-order must be 0 or more") + } + if err := validateChartRepositoryURL(f.repositoryURL); err != nil { + return f, err + } + var err error + if f.defaultValues, err = readValuesFlagFile(cmd, "file"); err != nil { + return f, err + } + if cmd.Flags().Lookup("locked-file") != nil { + if f.lockedValues, err = readValuesFlagFile(cmd, "locked-file"); err != nil { + return f, err + } + f.required, _ = cmd.Flags().GetBool("required") + } + return f, nil +} + +// readValuesFlagFile returns the content of the file of a flag, or "" when +// the flag is empty. +func readValuesFlagFile(cmd *cobra.Command, flag string) (string, error) { + path, _ := cmd.Flags().GetString(flag) + if path == "" { + return "", nil + } + if err := checkNoPathTraversal(path); err != nil { + return "", err + } + path = filepath.Clean(path) + data, err := os.ReadFile(path) + if err != nil { + return "", readFileErr(path, err) + } + return string(data), nil +} + +// ---------- definition add-chart / remove-chart ---------- + +var definitionAddChartCmd = &cobra.Command{ + Use: "add-chart ", + Short: "Add a chart to a definition", + Long: `Add a chart to a stack definition. Needs the devops or admin role. + + is a definition name or ID. The server checks the chart source +against its allowed chart repositories (ALLOWED_CHART_REPOS). + +Examples: + stackctl definition add-chart example-dev --name worker --repository-url oci://registry.example.com/helm --chart-version 1.2.0 + stackctl definition add-chart example-dev --name api --chart-path /charts/api --deploy-order 2 --file api-values.yaml`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + f, err := readChartFlags(cmd) + if err != nil { + return err + } + c, err := newClient() + if err != nil { + return err + } + defID, err := resolveDefinitionID(c, args[0]) + if err != nil { + return err + } + order := f.deployOrder + chart, err := c.AddDefinitionChart(defID, &types.UpdateChartConfigRequest{ + ChartName: f.name, + RepositoryURL: f.repositoryURL, + ChartPath: f.chartPath, + ChartVersion: f.chartVersion, + SourceRepoURL: f.sourceRepoURL, + BuildPipelineID: f.buildPipelineID, + DeployOrder: &order, + DefaultValues: f.defaultValues, + }) + if err != nil { + return err + } + return printChartConfig(chart) + }, +} + +var definitionRemoveChartCmd = &cobra.Command{ + Use: "remove-chart ", + Short: "Remove a chart from a definition", + Long: `Remove a chart from a stack definition. Needs the devops or admin role. + + is a definition name or ID. is a chart name or ID of +that definition. A definition that is linked to a template cannot lose a +chart that the template requires (the server refuses). The command asks for +confirmation unless --yes is set. Running stacks keep the chart until their +next deploy. + +Examples: + stackctl definition remove-chart example-dev worker + stackctl definition remove-chart example-dev worker --yes`, + Args: cobra.ExactArgs(2), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newClient() + if err != nil { + return err + } + defID, err := resolveDefinitionID(c, args[0]) + if err != nil { + return err + } + chartID, err := resolveDefinitionChartID(c, defID, args[1]) + if err != nil { + return err + } + ok, err := confirmAction(cmd, fmt.Sprintf("Remove chart %s from definition %s? (y/n): ", args[1], args[0])) + if err != nil { + return err + } + if !ok { + printer.PrintMessage("Aborted.") + return nil + } + if err := c.DeleteDefinitionChart(defID, chartID); err != nil { + return err + } + if printer.Quiet { + fmt.Fprintln(printer.Writer, chartID) + return nil + } + printer.PrintMessage("Removed chart %s from definition %s", chartID, defID) + return nil + }, +} + +// ---------- definition check-upgrade / upgrade ---------- + +var definitionCheckUpgradeCmd = &cobra.Command{ + Use: "check-upgrade ", + Short: "Check whether the source template has a newer version", + Long: `Check whether the source template of a definition has a newer published +version, and which charts an upgrade adds or changes. + +A chart that is only in the definition shows as "only in the definition"; +the upgrade keeps it. A chart shows as "modified" when its values, its +repository URL or another field differs. A definition without a source +template, or with an unpublished template, has no upgrade. + +-q prints "yes" or "no" (an upgrade is available or not). -o json and +-o yaml include chart_diffs with the values of each chart: the definition +now (left) and the template (right). + +Examples: + stackctl definition check-upgrade example-dev + stackctl definition check-upgrade example-dev -o json`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newClient() + if err != nil { + return err + } + defID, err := resolveDefinitionID(c, args[0]) + if err != nil { + return err + } + check, err := c.CheckDefinitionUpgrade(defID) + if err != nil { + return err + } + if printer.Quiet { + fmt.Fprintln(printer.Writer, yesNo(check.UpgradeAvailable)) + return nil + } + switch printer.Format { + case output.FormatJSON: + return printer.PrintJSON(check) + case output.FormatYAML: + return printer.PrintYAML(check) + default: + return printUpgradeCheck(c, defID, check) + } + }, +} + +// printUpgradeCheck prints the result of check-upgrade as text and a table +// of the chart changes. Without an upgrade the server sends no reason, so +// the command reads the template link of the definition. +func printUpgradeCheck(c *client.Client, defID string, check *types.UpgradeCheck) error { + if !check.UpgradeAvailable { + printer.PrintMessage("%s", noUpgradeReason(c, defID)) + return nil + } + printer.PrintMessage("Upgrade available: from version %s to version %s", + valueOrDash(client.SanitizeServerText(check.CurrentVersion)), client.SanitizeServerText(check.LatestVersion)) + rows := upgradeRows(check) + if len(rows) == 0 { + return nil + } + return printer.PrintTable([]string{"CHART", "CHANGE"}, rows) +} + +// noUpgradeReason explains why a definition has no upgrade. +func noUpgradeReason(c *client.Client, defID string) string { + def, err := c.GetDefinition(defID) + switch { + case err != nil: + return "No upgrade is available." + case def.SourceTemplateID == "": + return "No upgrade: the definition has no source template." + default: + return fmt.Sprintf("No upgrade: the definition has template version %s, and the template has no newer published version.", + valueOrDash(client.SanitizeServerText(def.SourceTemplateVersion))) + } +} + +// upgradeRows returns one row for each chart of the changes. The server +// marks a chart "modified" only for other values or another repository +// URL; a chart with differences in chart_diffs (for example another chart +// version) is also shown as modified. +func upgradeRows(check *types.UpgradeCheck) [][]string { + ch := check.Changes + if ch == nil { + return nil + } + differs := map[string]bool{} + for _, d := range check.ChartDiffs { + if d.HasDifferences { + differs[d.ChartName] = true + } + } + var rows [][]string + add := func(names []string, change string) { + for _, n := range names { + rows = append(rows, []string{client.SanitizeServerText(n), change}) + } + } + add(ch.ChartsAdded, "added") + add(ch.ChartsModified, "modified") + var unchanged []string + for _, n := range ch.ChartsUnchanged { + if differs[n] { + add([]string{n}, "modified") + } else { + unchanged = append(unchanged, n) + } + } + add(ch.ChartsRemoved, "only in the definition (kept)") + add(unchanged, "unchanged") + return rows +} + +// valueOrDash returns s, or "-" when s is empty. +func valueOrDash(s string) string { + if s == "" { + return "-" + } + return s +} + +var definitionUpgradeCmd = &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade a definition to the latest version of its template", + Long: `Upgrade a definition to the latest published version of its source +template. The upgrade adds the new charts. For the charts that are also in +the template, it replaces the default values with the values of the +template: changes that you made to those values in the definition are +lost. It also sets the repository URL, the deploy order and (when the +template has them) the chart version, chart path, source repository and +build pipeline. Charts that are only in the definition stay. Running stacks +get the change at their next deploy. "definition check-upgrade -o yaml" +shows the values of both sides. + +The command shows the changes first and asks for confirmation unless --yes +is set. Needs the owner of the definition, a devops user or an admin. + +Examples: + stackctl definition upgrade example-dev + stackctl definition upgrade example-dev --yes`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newClient() + if err != nil { + return err + } + defID, err := resolveDefinitionID(c, args[0]) + if err != nil { + return err + } + check, err := c.CheckDefinitionUpgrade(defID) + if err != nil { + return err + } + if !check.UpgradeAvailable { + // Nothing to do: the command shows why and succeeds. + switch { + case printer.Quiet: + return nil + case printer.Format == output.FormatJSON: + return printer.PrintJSON(check) + case printer.Format == output.FormatYAML: + return printer.PrintYAML(check) + default: + return printUpgradeCheck(c, defID, check) + } + } + + if yes, _ := cmd.Flags().GetBool("yes"); !yes { + if err := printUpgradeCheckTo(cmd, c, defID, check); err != nil { + return err + } + ok, err := confirmAction(cmd, fmt.Sprintf("Upgrade definition %s to version %s? The default values of the template charts replace the values of the definition. (y/n): ", + args[0], client.SanitizeServerText(check.LatestVersion))) + if err != nil { + return err + } + if !ok { + printer.PrintMessage("Aborted.") + return nil + } + } + + def, err := c.UpgradeDefinition(defID) + if err != nil { + return err + } + if printer.Format == output.FormatTable && !printer.Quiet { + printer.PrintMessage("Upgraded definition %s to version %s", defID, valueOrDash(client.SanitizeServerText(def.SourceTemplateVersion))) + return nil + } + return printDefinition(def) + }, +} + +// printUpgradeCheckTo prints the upgrade check as text to stderr, so the +// output of the command (stdout) stays the upgraded definition. +func printUpgradeCheckTo(cmd *cobra.Command, c *client.Client, defID string, check *types.UpgradeCheck) error { + prev := printer.Writer + printer.Writer = cmd.ErrOrStderr() + defer func() { printer.Writer = prev }() + return printUpgradeCheck(c, defID, check) +} + +// ---------- definition link-template ---------- + +var definitionLinkTemplateCmd = &cobra.Command{ + Use: "link-template ", + Short: "Link a definition to a template version, or remove the link", + Long: `Link a definition to a published template version, or remove the link +with --unlink. Needs the devops or admin role. The link changes only the +source template of the definition; its charts and values do not change. + +A linked definition gets the template rules: the required charts stay, the +locked values of the latest published template version apply at deploy, +and "definition check-upgrade" compares the definition with the template. +--version is a published version of the template; without it the server +uses the latest published version. The template must be published. + +Examples: + stackctl definition link-template example-dev --template web-stack + stackctl definition link-template example-dev --template web-stack --version 1.2.0 + stackctl definition link-template example-dev --unlink`, + Args: cobra.ExactArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + tmpl, _ := cmd.Flags().GetString("template") + version, _ := cmd.Flags().GetString("version") + unlink, _ := cmd.Flags().GetBool("unlink") + switch { + case unlink && (tmpl != "" || version != ""): + return fmt.Errorf("--unlink cannot be used with --template or --version") + case !unlink && strings.TrimSpace(tmpl) == "": + return fmt.Errorf("--template or --unlink is required") + } + + c, err := newClient() + if err != nil { + return err + } + defID, err := resolveDefinitionID(c, args[0]) + if err != nil { + return err + } + req := &types.TemplateLinkRequest{Version: version} + if !unlink { + if req.TemplateID, err = resolveTemplateID(c, tmpl); err != nil { + return err + } + } + def, err := c.SetDefinitionTemplateLink(defID, req) + if err != nil { + return linkTemplateError(err) + } + if printer.Format == output.FormatTable && !printer.Quiet { + if def.SourceTemplateID == "" { + printer.PrintMessage("Removed the template link of definition %s", defID) + } else { + printer.PrintMessage("Linked definition %s to template %s version %s", defID, def.SourceTemplateID, valueOrDash(client.SanitizeServerText(def.SourceTemplateVersion))) + } + return nil + } + return printDefinition(def) + }, +} + +// linkTemplateError adds an upgrade hint to the 404 of a server without +// the template-link route (before v0.11.0). +func linkTemplateError(err error) error { + if isStatus(err, http.StatusNotFound) && isRouteNotFound(err) { + return fmt.Errorf("%w (the server does not support template links; upgrade k8s-stack-manager to v0.11.0 or later)", err) + } + return err +} + +// isRouteNotFound reports whether a 404 has no JSON message: the route +// does not exist on the server. +func isRouteNotFound(err error) bool { + var apiErr *client.APIError + return errors.As(err, &apiErr) && apiErr.Message == http.StatusText(http.StatusNotFound) +} + +// ---------- template add-chart / remove-chart ---------- + +// templateDraftNote is written to stderr after a change of template charts. +const templateDraftNote = "Note: the change is in the working copy of the template. Run 'stackctl template publish %s --version ' to make it visible to users.\n" + +var templateAddChartCmd = &cobra.Command{ + Use: "add-chart