diff --git a/.golangci.yml b/.golangci.yml
index 2c7b25c2975..7dc561c2842 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -108,6 +108,7 @@ linters:
- '!**/integrations/operator/controllers/resources/testlib/**'
- '!**/lib/auth/test/**'
- '!**/lib/services/suite/**'
+ - '!**/build.assets/tooling/**'
deny:
- pkg: github.com/google/go-cmp/cmp
desc: '"github.com/google/go-cmp/cmp" should only be used in tests'
diff --git a/Makefile b/Makefile
index 2d5d47d2bdf..a8263cc8a2d 100644
--- a/Makefile
+++ b/Makefile
@@ -288,6 +288,14 @@ VERSRC = gitref.go api/version.go
KUBECONFIG ?=
TEST_KUBE ?=
+# This unexport statement is required for make to work with the `-e` flag.
+# With -e, the first Makefile sets HELMJANITOR=$$(go tool ...),
+# passes HELMJANITOR=$(go tool) to the child make process.
+# Because of the `-e` flag it takes precedence over the child's make definition
+# of HELMJANITOR and breaks environment variable expansion.
+# To avoid breaking other parts of the release pipeline, the easiest fix is to
+# unexport HELMJANITOR so the child uses the definition from its Makefile.
+unexport HELMJANITOR
export KUBECONFIG
export TEST_KUBE
@@ -950,25 +958,11 @@ helmunit/installed:
# environment variable.
.PHONY: test-helm
test-helm: helmunit/installed
- helm unittest -3 --with-subchart=false examples/chart/teleport-cluster
- helm unittest -3 --with-subchart=false examples/chart/teleport-kube-agent
- helm unittest -3 --with-subchart=false examples/chart/teleport-relay
- helm unittest -3 --with-subchart=false examples/chart/teleport-cluster/charts/teleport-operator
- helm unittest -3 --with-subchart=false examples/chart/access/*
- helm unittest -3 --with-subchart=false examples/chart/event-handler
- helm unittest -3 --with-subchart=false examples/chart/tbot
- helm unittest -3 --with-subchart=false examples/chart/tbot-spiffe-daemon-set
+ $(HELMJANITOR) test
.PHONY: test-helm-update-snapshots
test-helm-update-snapshots: helmunit/installed
- helm unittest -3 -u --with-subchart=false examples/chart/teleport-cluster
- helm unittest -3 -u --with-subchart=false examples/chart/teleport-kube-agent
- helm unittest -3 -u --with-subchart=false examples/chart/teleport-relay
- helm unittest -3 -u --with-subchart=false examples/chart/teleport-cluster/charts/teleport-operator
- helm unittest -3 -u --with-subchart=false examples/chart/access/*
- helm unittest -3 -u --with-subchart=false examples/chart/event-handler
- helm unittest -3 -u --with-subchart=false examples/chart/tbot
- helm unittest -3 -u --with-subchart=false examples/chart/tbot-spiffe-daemon-set
+ $(HELMJANITOR) test --update-snapshots
#
# Runs all Go tests except integration, called by CI/CD.
@@ -1382,30 +1376,8 @@ lint-sh:
# If errors are found, the file is printed with line numbers to aid in debugging.
.PHONY: lint-helm
lint-helm:
- @if ! type yamllint 2>&1 >/dev/null; then \
- echo "Not running 'lint-helm' target as 'yamllint' is not installed."; \
- if [ "$${CI}" = "true" ]; then echo "This is a failure when running in CI." && exit 1; fi; \
- exit 0; \
- fi; \
- for CHART in ./examples/chart/teleport-cluster ./examples/chart/teleport-kube-agent ./examples/chart/teleport-relay ./examples/chart/teleport-cluster/charts/teleport-operator ./examples/chart/tbot ./examples/chart/tbot-spiffe-daemon-set; do \
- if [ -d $${CHART}/.lint ]; then \
- for VALUES in $${CHART}/.lint/*.yaml; do \
- export HELM_TEMP=$$(mktemp); \
- echo -n "Using values from '$${VALUES}': "; \
- yamllint -c examples/chart/.lint-config.yaml $${VALUES} || { cat -en $${VALUES}; exit 1; }; \
- helm lint --quiet --strict $${CHART} -f $${VALUES} || exit 1; \
- helm template test $${CHART} -f $${VALUES} 1>$${HELM_TEMP} || exit 1; \
- yamllint -c examples/chart/.lint-config.yaml $${HELM_TEMP} || { cat -en $${HELM_TEMP}; exit 1; }; \
- echo; \
- done \
- else \
- export HELM_TEMP=$$(mktemp); \
- helm lint --quiet --strict $${CHART} || exit 1; \
- helm template test $${CHART} 1>$${HELM_TEMP} || exit 1; \
- yamllint -c examples/chart/.lint-config.yaml $${HELM_TEMP} || { cat -en $${HELM_TEMP}; exit 1; }; \
- fi; \
- done
- $(MAKE) -C examples/chart check-chart-ref
+ $(HELMJANITOR) reference -check
+ $(HELMJANITOR) lint
ADDLICENSE_COMMON_ARGS := -c 'Gravitational, Inc.' \
-ignore '**/*.c' \
diff --git a/build.assets/tooling/cmd/helm-janitor/lint.go b/build.assets/tooling/cmd/helm-janitor/lint.go
new file mode 100644
index 00000000000..7108afdde9c
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/lint.go
@@ -0,0 +1,159 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/gravitational/trace"
+)
+
+func runLint(ctx context.Context, charts []Chart, rootDir string) error {
+ // Preflight check ot make sure yamllint is installed
+ if err := checkDependencies(yamllintBinName, helmBinName); err != nil {
+ return trace.Wrap(err, "preflight checks")
+ }
+
+ configPath := filepath.Join(rootDir, yamlLintConfigPath)
+
+ for _, chart := range charts {
+ if chart.IsLibrary {
+ continue
+ }
+ fmt.Println("Running lint for chart:", chart.Name)
+ valuesDir := filepath.Join(chart.Path, ".lint")
+ content, err := os.ReadDir(valuesDir)
+ if err != nil && !trace.IsNotFound(trace.ConvertSystemError(err)) {
+ return trace.Wrap(err, "reading values directory for %s", chart.Path)
+ }
+
+ values := make([]os.DirEntry, 0, len(content))
+ for _, file := range content {
+ if file.IsDir() {
+ log.Println("Skipping directory", file.Name())
+ continue
+ }
+ ext := filepath.Ext(file.Name())
+ if ext != ".yaml" && ext != ".yml" {
+ log.Printf("Skipping non-yaml file %s", file.Name())
+ continue
+ }
+ values = append(values, file)
+ }
+
+ // If the chart has no lint directory or if it's empty, we lint with the default values.
+ if len(values) == 0 {
+ if err := lint(ctx, "", configPath, chart); err != nil {
+ return trace.Wrap(err, "linting chart %s with default values", chart.Path)
+ }
+ }
+
+ for _, file := range values {
+ if err := lint(ctx, filepath.Join(valuesDir, file.Name()), configPath, chart); err != nil {
+ return trace.Wrap(err, "linting chart %s", chart.Path)
+ }
+ }
+ }
+
+ fmt.Println(" ✅ Charts successfully linted.")
+ return nil
+}
+
+// lint runs all the lint operations on a chart for a given value file.
+// If the value file path is empty, the chart is linted with its default values.
+func lint(ctx context.Context, valuesPath, configPath string, chart Chart) error {
+ // Yamllint the values
+ if valuesPath != "" {
+ if stdout, stderr, err := run(ctx, yamllintBinName, "-c", configPath, valuesPath); err != nil {
+ fmt.Printf(" ❌ yamllint values %q failed\n", valuesPath)
+ // yamllint seems to output to stdout
+ fmt.Println(string(stdout))
+ fmt.Println(string(stderr))
+ return trace.Wrap(err, "linting values %q", valuesPath)
+ }
+ }
+
+ // Helm lint
+ args := []string{
+ "lint", "--quiet", "--strict", chart.Path,
+ }
+ if valuesPath != "" {
+ args = append(args, "-f", valuesPath)
+ }
+ if stdout, stderr, err := run(ctx, helmBinName, args...); err != nil {
+ fmt.Printf(" ❌ Helm linting chart %q failed with values %q\n", chart.Name, valuesPath)
+ fmt.Println(string(stdout))
+ fmt.Println(string(stderr))
+ return trace.Wrap(err, "linting with values %q", valuesPath)
+ }
+
+ // Render the manifests
+ tmpDest, err := os.CreateTemp("", "*-out.yaml")
+ if err != nil {
+ return trace.ConvertSystemError(err)
+ }
+ defer func() {
+ tmpDest.Close()
+ os.Remove(tmpDest.Name())
+ }()
+
+ args = []string{"template", "test", chart.Path}
+ if valuesPath != "" {
+ args = append(args, "-f", valuesPath)
+ }
+ rendered, stderr, err := run(ctx, helmBinName, args...)
+ if err != nil {
+ fmt.Println(string(stderr))
+ return trace.Wrap(err, "rendering templates for values %q", valuesPath)
+ }
+ if _, err := tmpDest.Write(rendered); err != nil {
+ return trace.ConvertSystemError(err)
+ }
+
+ // Yammllint the manifests
+ if stdout, stderr, err := run(ctx, yamllintBinName, "-c", configPath, tmpDest.Name()); err != nil {
+ fmt.Printf(" ❌ yamllint rendered chart %q with values %q failed\n", chart.Name, valuesPath)
+ // We output the linted template to stdout with line numbers to make finding and fixing the error easier.
+ fmt.Println(" 🔎 Linted templates:")
+ printWithLineNumbers(rendered)
+ fmt.Println()
+ fmt.Println(" ⚠️ Linting errors:")
+ fmt.Println(string(stdout))
+ fmt.Println(string(stderr))
+ return trace.Wrap(err, "linting rendered templates for values %q", valuesPath)
+ }
+
+ return nil
+}
+
+func printWithLineNumbers(stdout []byte) {
+ lines := strings.Split(string(stdout), "\n")
+ count := len(lines)
+ numDigit := len(strconv.FormatInt(int64(count), 10))
+
+ for i := 0; i < count; i++ {
+ fmt.Printf("%*d | %s\n", numDigit+1, i+1, lines[i])
+ }
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/main.go b/build.assets/tooling/cmd/helm-janitor/main.go
new file mode 100644
index 00000000000..e10d37f4f23
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/main.go
@@ -0,0 +1,259 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "os/signal"
+ "sort"
+ "syscall"
+
+ "github.com/gravitational/trace"
+ "gopkg.in/yaml.v3"
+)
+
+// Chart represents a chart we want to test, lint, publish a reference for, and update.
+type Chart struct {
+ // Name of the chart.
+ Name string
+ // Path of the chart, relative to the teleport repo root.
+ Path string
+ // ReferencePath is where the generated reference is stored.
+ // When it's empty, no reference is generated.
+ ReferencePath string
+ // IsLibrary describes if the chart is a library chart.
+ // Library charts cannot be installed and are not directly tested, nor linted.
+ IsLibrary bool
+}
+
+// charts is the source of truth for the list of charts we maintain.
+// If you need to introduce a new chart, add to this list.
+var charts = []Chart{
+ {
+ Name: "teleport-cluster",
+ Path: "examples/chart/teleport-cluster",
+ // teleport-cluster reference is still hand-written.
+ ReferencePath: "",
+ },
+ {
+ Name: "teleport-kube-agent",
+ Path: "examples/chart/teleport-kube-agent",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.teleport-kube-agent.mdx",
+ },
+ {
+ Name: "teleport-relay",
+ Path: "examples/chart/teleport-relay",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.teleport-relay.mdx",
+ },
+ {
+ Name: "teleport-operator",
+ Path: "examples/chart/teleport-cluster/charts/teleport-operator",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.teleport-operator.mdx",
+ },
+ {
+ Name: "access-email",
+ Path: "examples/chart/access/email",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-email.mdx",
+ },
+ {
+ Name: "access-jira",
+ Path: "examples/chart/access/jira",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-jira.mdx",
+ },
+ {
+ Name: "access-mattermost",
+ Path: "examples/chart/access/mattermost",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-mattermost.mdx",
+ },
+ {
+ Name: "access-msteams",
+ Path: "examples/chart/access/msteams",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-msteams.mdx",
+ },
+ {
+ Name: "access-pagerduty",
+ Path: "examples/chart/access/pagerduty",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-pagerduty.mdx",
+ },
+ {
+ Name: "access-slack",
+ Path: "examples/chart/access/slack",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-slack.mdx",
+ },
+ {
+ Name: "access-discord",
+ Path: "examples/chart/access/discord",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-discord.mdx",
+ },
+ {
+ Name: "access-datadog",
+ Path: "examples/chart/access/datadog",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.access-datadog.mdx",
+ },
+ {
+ Name: "event-handler",
+ Path: "examples/chart/event-handler",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.event-handler.mdx",
+ },
+ {
+ Name: "tbot",
+ Path: "examples/chart/tbot",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.tbot.mdx",
+ },
+ {
+ Name: "tbot-spiffe-daemon-set",
+ Path: "examples/chart/tbot-spiffe-daemon-set",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.tbot-spiffe-daemon-set.mdx",
+ },
+ {
+ Name: "teleport-kube-updater",
+ Path: "examples/chart/teleport-kube-updater",
+ ReferencePath: "",
+ IsLibrary: true,
+ },
+}
+
+const usage = `Usage:
+ helm-janitor all [--charts=] [--root-dir=]
+ helm-janitor test [--charts=] [--root-dir=]
+ helm-janitor lint [--charts=] [--root-dir=]
+ helm-janitor reference [--check] [--charts=] [--root-dir=]
+
+ helm-janitor list [--root-dir=]
+ helm-janitor update-version [--root-dir=]
+
+ is a comma-separated list of chart names.
+ is the path to the teleport repo root.
+`
+
+func main() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ch := make(chan os.Signal, 1)
+ signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ select {
+ case <-ch:
+ cancel()
+ case <-ctx.Done():
+ }
+ }()
+
+ if len(os.Args) < 2 {
+ fmt.Fprint(os.Stderr, usage)
+ os.Exit(1)
+ }
+ command := os.Args[1]
+
+ fs := flag.NewFlagSet("helm-janitor", flag.ExitOnError)
+ chartsFlag := fs.String("charts", "", "Comma-separated list of chart names")
+ updateSnapshotsFlag := fs.Bool("update-snapshots", false, "Update Helm test snapshots")
+ checkFlag := fs.Bool("check", false, "Check if references are up to date")
+ rootDirFlag := fs.String("root-dir", "", "Root directory of the teleport repo.")
+
+ if err := fs.Parse(os.Args[2:]); err != nil {
+ log.Fatal(err)
+ }
+
+ selectedCharts, err := selectCharts(*chartsFlag, *rootDirFlag)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ switch command {
+ case "all":
+ if err := runAll(ctx, selectedCharts, *rootDirFlag); err != nil {
+ log.Fatal(err)
+ }
+
+ case "test":
+ if err := runTest(ctx, selectedCharts, *updateSnapshotsFlag); err != nil {
+ log.Fatal(err)
+ }
+
+ case "lint":
+ if err := runLint(ctx, selectedCharts, *rootDirFlag); err != nil {
+ log.Fatal(err)
+ }
+
+ case "reference", "ref":
+ if err := runReference(ctx, selectedCharts, *checkFlag); err != nil {
+ log.Fatal(err)
+ }
+
+ case "list":
+ if err := listCharts(ctx, selectedCharts); err != nil {
+ log.Fatal(err)
+ }
+
+ case "update-version":
+ args := fs.Args()
+ if len(args) != 1 {
+ fmt.Fprintln(os.Stderr, "Error: update-version requires exactly one argument (version)")
+ fmt.Fprint(os.Stderr, usage)
+ os.Exit(1)
+ }
+ version := args[0]
+
+ if err := updateVersion(ctx, version, selectedCharts); err != nil {
+ log.Fatal(err)
+ }
+
+ default:
+ fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", command)
+ fmt.Fprint(os.Stderr, usage)
+ os.Exit(1)
+ }
+}
+
+func runAll(ctx context.Context, charts []Chart, rootDir string) error {
+ fmt.Println("Running all operations...")
+ if err := runLint(ctx, charts, rootDir); err != nil {
+ return trace.Wrap(err)
+ }
+ const updateSnapshots = false
+ if err := runTest(ctx, charts, updateSnapshots); err != nil {
+ return trace.Wrap(err)
+ }
+ if err := runReference(ctx, charts, false); err != nil {
+ return trace.Wrap(err)
+ }
+ return nil
+}
+
+func listCharts(ctx context.Context, charts []Chart) error {
+ fmt.Println("Available charts:")
+ paths := make([]string, len(charts))
+ for i, chart := range charts {
+ paths[i] = chart.Path
+ }
+ sort.Strings(paths)
+ out, err := yaml.Marshal(paths)
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(out))
+ return nil
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/reference.go b/build.assets/tooling/cmd/helm-janitor/reference.go
new file mode 100644
index 00000000000..1a01f49a4d8
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/reference.go
@@ -0,0 +1,68 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport/build.assets/tooling/lib/helm"
+)
+
+func runReference(ctx context.Context, charts []Chart, check bool) error {
+ for _, chart := range charts {
+ if chart.ReferencePath == "" {
+ // teleport-cluster's reference is not yet rendered
+ continue
+ }
+ fmt.Printf("Rendering reference for chart %s\n", chart.Path)
+ ref, err := helm.RenderReference(chart.Path)
+ if err != nil {
+ return trace.Wrap(err, "rendering chart reference for %q", chart.Path)
+ }
+ if check {
+ existing, err := os.ReadFile(chart.ReferencePath)
+ if err != nil {
+ return trace.ConvertSystemError(err)
+ }
+ if !bytes.Equal(existing, ref) {
+ fmt.Printf(" ❌ Out-of-sync reference for chart %q.\n", chart.Name)
+ fmt.Println("Please run `make -C example/chart render-chart-ref`")
+ fmt.Println()
+ fmt.Println(cmp.Diff(string(existing), string(ref)))
+ return trace.CompareFailed("reference is out of date for chart %q", chart.Path)
+ }
+ continue
+ }
+ if err := os.WriteFile(chart.ReferencePath, ref, 0644); err != nil {
+ return trace.ConvertSystemError(err)
+ }
+ }
+ if check {
+ fmt.Println(" ✅ All references are up-to-date")
+ } else {
+ fmt.Println(" ✅ All references rendered")
+ }
+ return nil
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/test.go b/build.assets/tooling/cmd/helm-janitor/test.go
new file mode 100644
index 00000000000..526c793f583
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/test.go
@@ -0,0 +1,63 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/gravitational/trace"
+)
+
+func runTest(ctx context.Context, charts []Chart, updateSnapshots bool) error {
+ if err := checkDependencies(helmBinName); err != nil {
+ return trace.Wrap(err, "preflight checks")
+ }
+
+ for _, chart := range charts {
+ if chart.IsLibrary {
+ continue
+ }
+ if err := testHelm(ctx, chart, updateSnapshots); err != nil {
+ return trace.Wrap(err)
+ }
+ }
+
+ fmt.Println(" ✅ All tests succeeded")
+ return nil
+}
+
+func testHelm(ctx context.Context, chart Chart, updateSnapshots bool) error {
+ fmt.Println("Running tests for chart:", chart.Name)
+ args := []string{"unittest", "-3", "--with-subchart=false", chart.Path}
+ if updateSnapshots {
+ args = append(args, "-u")
+ }
+ // We log the test command so it's easier for a developer to copy it and re-run to target a failing test.
+ fmt.Printf(" ▶️ %s %s\n", helmBinName, strings.Join(args, " "))
+ stdout, stderr, err := run(ctx, helmBinName, args...)
+ if err != nil {
+ fmt.Printf(" ❌ Helm unit tests failed for chart %q", chart.Path)
+ fmt.Println(string(stdout))
+ fmt.Println(string(stderr))
+ return trace.Wrap(err, "testing chart %q", chart.Path)
+ }
+ return nil
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/utils.go b/build.assets/tooling/cmd/helm-janitor/utils.go
new file mode 100644
index 00000000000..70e3449d83e
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/utils.go
@@ -0,0 +1,116 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/gravitational/trace"
+)
+
+const (
+ yamllintBinName = "yamllint"
+ helmBinName = "helm"
+ yamlLintConfigPath = "examples/chart/.lint-config.yaml"
+)
+
+func checkDependencies(names ...string) error {
+ for _, name := range names {
+ _, err := exec.LookPath(name)
+ if err != nil {
+ return trace.NotFound("%s not found in $PATH", name)
+ }
+ }
+ return nil
+}
+
+func run(ctx context.Context, command string, args ...string) ([]byte, []byte, error) {
+ cmd := exec.CommandContext(ctx, command, args...)
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ cmd.Env = os.Environ()
+ if err := cmd.Run(); err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ return stdout.Bytes(), stderr.Bytes(), trace.Wrap(err, "command %s exited with status %d", command, exitErr.ExitCode())
+ }
+ return stdout.Bytes(), stderr.Bytes(), trace.Wrap(err)
+ }
+ return stdout.Bytes(), stderr.Bytes(), nil
+}
+
+func chartsWithPath(rootDir string) []Chart {
+ if rootDir == "" {
+ rootDir = "."
+ }
+ pathedCharts := make([]Chart, len(charts))
+ for i, chart := range charts {
+ var path, referencePath string
+ if chart.Path != "" {
+ path = filepath.Join(rootDir, chart.Path)
+ }
+ if chart.ReferencePath != "" {
+ referencePath = filepath.Join(rootDir, chart.ReferencePath)
+ }
+ pathedCharts[i] = Chart{
+ Name: chart.Name,
+ Path: path,
+ ReferencePath: referencePath,
+ IsLibrary: chart.IsLibrary,
+ }
+ }
+ return pathedCharts
+}
+
+func selectCharts(chartNames string, rootDir string) ([]Chart, error) {
+ charts := chartsWithPath(rootDir)
+ if chartNames == "" {
+ return charts, nil
+ }
+
+ validNameSet := make(map[string]struct{})
+ for _, chart := range charts {
+ validNameSet[chart.Name] = struct{}{}
+ }
+
+ selectedNames := strings.Split(chartNames, ",")
+ selectedNameSet := make(map[string]struct{})
+ for _, name := range selectedNames {
+ if _, ok := validNameSet[name]; !ok {
+ return nil, trace.NotFound("unknown chart name: %s", name)
+ }
+ selectedNameSet[strings.TrimSpace(name)] = struct{}{}
+ }
+
+ var selected []Chart
+ for _, chart := range charts {
+ if _, ok := selectedNameSet[chart.Name]; ok {
+ selected = append(selected, chart)
+ }
+ }
+
+ return selected, nil
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/utils_test.go b/build.assets/tooling/cmd/helm-janitor/utils_test.go
new file mode 100644
index 00000000000..d1828734b5b
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/utils_test.go
@@ -0,0 +1,115 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSelectCharts(t *testing.T) {
+ tests := []struct {
+ name string
+ chartNames string
+ rootDir string
+ expected []Chart
+ expectErr require.ErrorAssertionFunc
+ }{
+ {
+ name: "no chart name should select all charts",
+ chartNames: "",
+ rootDir: "",
+ expected: chartsWithPath("."),
+ expectErr: require.NoError,
+ },
+ {
+ name: "single chart name",
+ chartNames: "teleport-kube-agent",
+ rootDir: "",
+ expected: []Chart{
+ {
+ Name: "teleport-kube-agent",
+ Path: "examples/chart/teleport-kube-agent",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.teleport-kube-agent.mdx",
+ },
+ },
+ expectErr: require.NoError,
+ },
+ {
+ name: "multiple chart name",
+ chartNames: "teleport-kube-agent,teleport-relay",
+ rootDir: "",
+ expected: []Chart{
+ {
+ Name: "teleport-kube-agent",
+ Path: "examples/chart/teleport-kube-agent",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.teleport-kube-agent.mdx",
+ },
+ {
+ Name: "teleport-relay",
+ Path: "examples/chart/teleport-relay",
+ ReferencePath: "docs/pages/includes/helm-reference/zz_generated.teleport-relay.mdx",
+ },
+ },
+ expectErr: require.NoError,
+ },
+ {
+ name: "single chart name with root dir",
+ chartNames: "teleport-kube-agent",
+ rootDir: "/tmp/teleport",
+ expected: []Chart{
+ {
+ Name: "teleport-kube-agent",
+ Path: "/tmp/teleport/examples/chart/teleport-kube-agent",
+ ReferencePath: "/tmp/teleport/docs/pages/includes/helm-reference/zz_generated.teleport-kube-agent.mdx",
+ },
+ },
+ expectErr: require.NoError,
+ },
+ {
+ name: "single library chart",
+ chartNames: "teleport-kube-updater",
+ rootDir: "",
+ expected: []Chart{
+ {
+ Name: "teleport-kube-updater",
+ Path: "examples/chart/teleport-kube-updater",
+ IsLibrary: true,
+ },
+ },
+ expectErr: require.NoError,
+ },
+ {
+ name: "unknown chart name",
+ chartNames: "unknown-chart",
+ rootDir: "",
+ expected: nil,
+ expectErr: require.Error,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result, err := selectCharts(test.chartNames, test.rootDir)
+ test.expectErr(t, err)
+ require.Equal(t, test.expected, result)
+ })
+ }
+
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/version.go b/build.assets/tooling/cmd/helm-janitor/version.go
new file mode 100644
index 00000000000..d412dc1c725
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/version.go
@@ -0,0 +1,60 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/gravitational/trace"
+)
+
+func updateVersion(ctx context.Context, version string, charts []Chart) error {
+ for _, chart := range charts {
+ if err := updateChartVersion(ctx, chart, version); err != nil {
+ return trace.Wrap(err, "updating version of chart %q", chart.Name)
+ }
+ }
+ fmt.Printf(" ✅ Version updated to %s\n", version)
+ return nil
+}
+
+var versionRegex = regexp.MustCompile(`\.version: .*`)
+
+func updateChartVersion(ctx context.Context, chart Chart, version string) error {
+ version = strings.TrimPrefix(version, "v")
+ chartYaml, err := os.ReadFile(filepath.Join(chart.Path, "Chart.yaml"))
+ if err != nil {
+ return trace.Wrap(trace.ConvertSystemError(err), "reading Chart.yaml")
+ }
+ newChartYaml := versionRegex.ReplaceAll(chartYaml, []byte(fmt.Sprintf(`.version: &version %q`, version)))
+ if bytes.Equal(chartYaml, newChartYaml) {
+ fmt.Printf(" ⚠️ Warning: Chart.yaml unchanged: %q\n", chart.Path)
+ }
+ if err := os.WriteFile(filepath.Join(chart.Path, "Chart.yaml"), newChartYaml, 0644); err != nil {
+ return trace.Wrap(err, "writing Chart.yaml")
+ }
+
+ return nil
+}
diff --git a/build.assets/tooling/cmd/helm-janitor/version_test.go b/build.assets/tooling/cmd/helm-janitor/version_test.go
new file mode 100644
index 00000000000..b78d74179eb
--- /dev/null
+++ b/build.assets/tooling/cmd/helm-janitor/version_test.go
@@ -0,0 +1,67 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ testChartYaml = `
+.version: &version "1.2.3"
+
+name: test-chart
+version: *version
+appVersion: *version
+
+dependencies:
+ - name: other-test-chart
+ version: *version
+`
+ updatedTestChartYaml = `
+.version: &version "1.2.4-foobar"
+
+name: test-chart
+version: *version
+appVersion: *version
+
+dependencies:
+ - name: other-test-chart
+ version: *version
+`
+)
+
+func TestUpdateChartVersion(t *testing.T) {
+ dir := t.TempDir()
+ chart := Chart{
+ Name: "test-chart",
+ Path: dir,
+ }
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "Chart.yaml"), []byte(testChartYaml), 0644))
+ require.NoError(t, updateChartVersion(t.Context(), chart, "1.2.4-foobar"))
+ require.FileExists(t, filepath.Join(dir, "Chart.yaml"))
+ updatedChart, err := os.ReadFile(filepath.Join(dir, "Chart.yaml"))
+ require.NoError(t, err)
+ require.Equal(t, updatedTestChartYaml, string(updatedChart))
+
+}
diff --git a/build.assets/tooling/cmd/render-helm-ref/README.md b/build.assets/tooling/cmd/render-helm-ref/README.md
index 5927b426705..08350c7451a 100644
--- a/build.assets/tooling/cmd/render-helm-ref/README.md
+++ b/build.assets/tooling/cmd/render-helm-ref/README.md
@@ -13,8 +13,8 @@ go run ./cmd/render-helm-ref/ \
### `values.yaml` syntax
-See [the test data `values.yaml`](./testdata/values.yaml) for an example input,
-and [its expected output](./testdata/values.yaml)
+See [the test data `values.yaml`](../../lib/helm/testdata/values.yaml) for an example input,
+and [its expected output](../../lib/helm/testdata/values.yaml)
### Why?
diff --git a/build.assets/tooling/cmd/render-helm-ref/main.go b/build.assets/tooling/cmd/render-helm-ref/main.go
index 5cfa13b35eb..7a97e6317e4 100644
--- a/build.assets/tooling/cmd/render-helm-ref/main.go
+++ b/build.assets/tooling/cmd/render-helm-ref/main.go
@@ -19,33 +19,15 @@
package main
import (
- "bufio"
"context"
- "encoding/json"
"flag"
"fmt"
"log/slog"
"os"
- "regexp"
- "strings"
- "github.com/gravitational/trace"
- "gopkg.in/yaml.v3"
- "helm.sh/helm/v3/pkg/chart/loader"
+ "github.com/gravitational/teleport/build.assets/tooling/lib/helm"
)
-// tagRegex matches tagged values with kind. For example:
-// # full.path.valueName(kind) -- controls how to do X
-var tagRegex = regexp.MustCompile(`^\s*#\s*(.*)\((.+)\)\s+--\s*(.*)$`)
-
-// tagRegex matches tagged values without kind. For example:
-// # full.path.valueName -- controls how to do X
-var tagRegexNoKind = regexp.MustCompile(`^\s*#\s*(.*)\s+--\s*(.*)$`)
-
-// defaultRegex matches the default override tag. For example:
-// # @default -- see values.yaml
-var defaultRegex = regexp.MustCompile(`^\s*# @default -- (.*)$`)
-
func main() {
var chartPath string
var outputPath string
@@ -59,7 +41,7 @@ func main() {
os.Exit(1)
}
- reference, err := parseAndRender(chartPath)
+ reference, err := helm.RenderReference(chartPath)
if err != nil {
slog.ErrorContext(ctx, "failed parsing chart and rendering reference", "error", err)
os.Exit(1)
@@ -76,192 +58,3 @@ func main() {
}
slog.InfoContext(ctx, "File successfully written", "file_path", outputPath)
}
-
-func parseAndRender(chartPath string) ([]byte, error) {
- chartValues := chartPath + "/" + "values.yaml"
-
- // First, we check if we can load all the data we need.
- // We need both the Helm chart (for automatic default detection)
- chrt, err := loader.Load(chartPath)
- if err != nil {
- return nil, trace.Wrap(err, "failed to load chart")
- }
-
- // And the raw values.yaml (for the documentation in comments)
- valuesYAML, err := os.ReadFile(chartValues)
- if err != nil {
- return nil, trace.Wrap(err, "failed to open values '%s'", chartValues)
- }
-
- // We parse the YAML and extract all the documented values form its comments
- var n yaml.Node
- err = yaml.Unmarshal(valuesYAML, &n)
- if err != nil {
- return nil, trace.Wrap(err, "failed to unmarshall values")
- }
- values := processYAMLNode(&n)
-
- // Then, we backfill the default value when possible
- for _, value := range values {
- // We can skip the default detection by setting no kind. This is useful when
- // we are also documenting subfields and don't want an ugly Type/Default table.
- if value.Kind != "" && value.Default == "" {
- defaultValue, err := getDefaultForValue(value.Name, chrt.Values)
- if err != nil {
- slog.WarnContext(context.Background(), "failed to look up default value",
- "value", value.Name,
- "error", err,
- )
- } else {
- value.Default = string(defaultValue)
- }
- }
- }
-
- // Finally we render
- reference, err := renderTemplate(values)
- return reference, trace.Wrap(err, "failed to render template")
-}
-
-func processYAMLNode(node *yaml.Node) []*Value {
- // The YAML structure does not represent the value hierarchy
- // So we process all comments the same way and don't care about their position
- var values []*Value
- if value := grabValue(node.HeadComment); value != nil {
- values = append(values, value)
- }
- if value := grabValue(node.LineComment); value != nil {
- values = append(values, value)
- }
- if value := grabValue(node.FootComment); value != nil {
- values = append(values, value)
- }
- if len(node.Content) != 0 {
- for _, subNode := range node.Content {
- values = append(values, processYAMLNode(subNode)...)
- }
- }
- return values
-}
-
-type Value struct {
- Name string
- Kind string
-
- Description string
- Default string
-}
-
-type state struct {
- isDescription bool
- description strings.Builder
-
- value *Value
-}
-
-// grabValue walks through a comment and checks if it has a value tag.
-// Once the value tag is found, everything after it will be part of the description.
-func grabValue(comment string) *Value {
- if comment == "" {
- return nil
- }
-
- scanner := bufio.NewScanner(strings.NewReader(comment))
-
- var line string
-
- s := state{}
- for scanner.Scan() {
- line = scanner.Text()
- if !s.isDescription {
- // We are not yet in a comment containing documentation
- match, name, kind, remain := matchTag(line)
- if !match {
- // no tag on this line, we skip it
- continue
- }
- // start of a value documentation
- s.isDescription = true
- s.value = &Value{Name: name, Kind: kind}
- s.description.WriteString(remain)
- s.description.WriteRune('\n')
- continue
- }
- // We already saw a tag on a previous line
- // If we find a default override tag we process it, else we just add the
- // line to the existing value description.
- if match, defaultValue := matchDefaultTag(line); match {
- s.value.Default = defaultValue
- continue
- }
- s.description.WriteString(cleanLine(line))
- s.description.WriteRune('\n')
- }
-
- if s.isDescription {
- s.value.Description = strings.TrimSpace(s.description.String())
- }
- return s.value
-}
-
-func matchTag(line string) (match bool, name, kind, remain string) {
- // If kind is specified
- subMatches := tagRegex.FindStringSubmatch(line)
- if len(subMatches) == 4 && subMatches[1] != "" {
- return true, subMatches[1], subMatches[2], subMatches[3]
- }
- // If kind is not specified
- subMatches = tagRegexNoKind.FindStringSubmatch(line)
- if len(subMatches) == 3 && subMatches[1] != "" {
- return true, subMatches[1], "", subMatches[2]
- }
- return false, "", "", ""
-}
-
-func matchDefaultTag(line string) (bool, string) {
- subMatches := defaultRegex.FindStringSubmatch(line)
- if len(subMatches) != 2 {
- return false, ""
- }
- return true, subMatches[1]
-}
-
-func cleanLine(line string) string {
- line2 := strings.TrimSpace(line)
- if len(line2) < 3 {
- return ""
- }
- if line2[0] != '#' {
- slog.WarnContext(context.Background(), "Misformatted line", "line", line)
- return ""
- }
- return line2[2:]
-}
-
-// getDefaultForValue takes a value detected from the comments, and looks up its
-// default value in the Helm chart.
-func getDefaultForValue(valueName string, chartValues map[string]interface{}) ([]byte, error) {
- parts := strings.Split(valueName, ".")
- // Check if this is a nested value
- if len(parts) > 1 {
- chartValue, ok := chartValues[parts[0]]
- if !ok {
- // Stop if the value is unknown
- return nil, trace.NotFound("value '%s' not found", parts[0])
- }
-
- // The value name is part0.part1...partX
- // We expect the detected Helm value to be a map, else we don't know
- // how to access "part1...partX"
- if subValue, ok := chartValue.(map[string]interface{}); ok {
- return getDefaultForValue(strings.Join(parts[1:], "."), subValue)
- }
- return nil, trace.CompareFailed("value %s cannot be cast to a map", parts[0])
-
- }
- // If the value is known we marshall it as JSON
- if chartValue, ok := chartValues[parts[0]]; ok {
- return json.Marshal(chartValue)
- }
- return nil, trace.NotFound("value '%s' not found", parts[0])
-}
diff --git a/build.assets/tooling/go.mod b/build.assets/tooling/go.mod
index 8962e196137..746c45fca4f 100644
--- a/build.assets/tooling/go.mod
+++ b/build.assets/tooling/go.mod
@@ -2,6 +2,8 @@ module github.com/gravitational/teleport/build.assets/tooling
go 1.25.9
+tool github.com/gravitational/teleport/build.assets/tooling/cmd/helm-janitor
+
require (
buf.build/go/bufplugin v0.9.0
github.com/DataDog/datadog-agent/pkg/template v0.77.2
@@ -26,6 +28,8 @@ require (
k8s.io/apiextensions-apiserver v0.35.1
)
+require github.com/google/go-cmp v0.7.0
+
require (
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.3-20250121211742-6d880cc6cc8d.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
@@ -147,7 +151,6 @@ require (
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-attestation v0.6.0 // indirect
- github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.21.4 // indirect
github.com/google/go-github/v70 v70.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
diff --git a/build.assets/tooling/cmd/render-helm-ref/main_test.go b/build.assets/tooling/lib/helm/main_test.go
similarity index 92%
rename from build.assets/tooling/cmd/render-helm-ref/main_test.go
rename to build.assets/tooling/lib/helm/main_test.go
index c6f4551e39a..c83c888a18e 100644
--- a/build.assets/tooling/cmd/render-helm-ref/main_test.go
+++ b/build.assets/tooling/lib/helm/main_test.go
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package main
+package helm
import (
"os"
@@ -30,14 +30,14 @@ const (
testSnapshotPath = "./testdata/expected-output.mdx"
)
-func Test_parseAndRender(t *testing.T) {
+func Test_RenderReference(t *testing.T) {
// Test setup: we load the fixtures
expected, err := os.ReadFile(testSnapshotPath)
require.NoError(t, err)
require.NotEmpty(t, expected)
// Test execution: we render templates, expect no error and check result
- actual, err := parseAndRender(testChartPath)
+ actual, err := RenderReference(testChartPath)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
diff --git a/build.assets/tooling/lib/helm/reference.go b/build.assets/tooling/lib/helm/reference.go
new file mode 100644
index 00000000000..2f09bf3fceb
--- /dev/null
+++ b/build.assets/tooling/lib/helm/reference.go
@@ -0,0 +1,234 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package helm
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "log/slog"
+ "os"
+ "regexp"
+ "strings"
+
+ "github.com/gravitational/trace"
+ "gopkg.in/yaml.v3"
+ "helm.sh/helm/v3/pkg/chart/loader"
+)
+
+// tagRegex matches tagged values with kind. For example:
+// # full.path.valueName(kind) -- controls how to do X
+var tagRegex = regexp.MustCompile(`^\s*#\s*(.*)\((.+)\)\s+--\s*(.*)$`)
+
+// tagRegex matches tagged values without kind. For example:
+// # full.path.valueName -- controls how to do X
+var tagRegexNoKind = regexp.MustCompile(`^\s*#\s*(.*)\s+--\s*(.*)$`)
+
+// defaultRegex matches the default override tag. For example:
+// # @default -- see values.yaml
+var defaultRegex = regexp.MustCompile(`^\s*# @default -- (.*)$`)
+
+func RenderReference(chartPath string) ([]byte, error) {
+ chartValues := chartPath + "/" + "values.yaml"
+
+ // First, we check if we can load all the data we need.
+ // We need both the Helm chart (for automatic default detection)
+ chrt, err := loader.Load(chartPath)
+ if err != nil {
+ return nil, trace.Wrap(err, "failed to load chart")
+ }
+
+ // And the raw values.yaml (for the documentation in comments)
+ valuesYAML, err := os.ReadFile(chartValues)
+ if err != nil {
+ return nil, trace.Wrap(err, "failed to open values '%s'", chartValues)
+ }
+
+ // We parse the YAML and extract all the documented values form its comments
+ var n yaml.Node
+ err = yaml.Unmarshal(valuesYAML, &n)
+ if err != nil {
+ return nil, trace.Wrap(err, "failed to unmarshall values")
+ }
+ values := processYAMLNode(&n)
+
+ // Then, we backfill the default value when possible
+ for _, value := range values {
+ // We can skip the default detection by setting no kind. This is useful when
+ // we are also documenting subfields and don't want an ugly Type/Default table.
+ if value.Kind != "" && value.Default == "" {
+ defaultValue, err := getDefaultForValue(value.Name, chrt.Values)
+ if err != nil {
+ slog.WarnContext(context.Background(), "failed to look up default value",
+ "value", value.Name,
+ "error", err,
+ )
+ } else {
+ value.Default = string(defaultValue)
+ }
+ }
+ }
+
+ // Finally we render
+ reference, err := renderTemplate(values)
+ return reference, trace.Wrap(err, "failed to render template")
+}
+
+func processYAMLNode(node *yaml.Node) []*Value {
+ // The YAML structure does not represent the value hierarchy
+ // So we process all comments the same way and don't care about their position
+ var values []*Value
+ if value := grabValue(node.HeadComment); value != nil {
+ values = append(values, value)
+ }
+ if value := grabValue(node.LineComment); value != nil {
+ values = append(values, value)
+ }
+ if value := grabValue(node.FootComment); value != nil {
+ values = append(values, value)
+ }
+ if len(node.Content) != 0 {
+ for _, subNode := range node.Content {
+ values = append(values, processYAMLNode(subNode)...)
+ }
+ }
+ return values
+}
+
+type Value struct {
+ Name string
+ Kind string
+
+ Description string
+ Default string
+}
+
+type state struct {
+ isDescription bool
+ description strings.Builder
+
+ value *Value
+}
+
+// grabValue walks through a comment and checks if it has a value tag.
+// Once the value tag is found, everything after it will be part of the description.
+func grabValue(comment string) *Value {
+ if comment == "" {
+ return nil
+ }
+
+ scanner := bufio.NewScanner(strings.NewReader(comment))
+
+ var line string
+
+ s := state{}
+ for scanner.Scan() {
+ line = scanner.Text()
+ if !s.isDescription {
+ // We are not yet in a comment containing documentation
+ match, name, kind, remain := matchTag(line)
+ if !match {
+ // no tag on this line, we skip it
+ continue
+ }
+ // start of a value documentation
+ s.isDescription = true
+ s.value = &Value{Name: name, Kind: kind}
+ s.description.WriteString(remain)
+ s.description.WriteRune('\n')
+ continue
+ }
+ // We already saw a tag on a previous line
+ // If we find a default override tag we process it, else we just add the
+ // line to the existing value description.
+ if match, defaultValue := matchDefaultTag(line); match {
+ s.value.Default = defaultValue
+ continue
+ }
+ s.description.WriteString(cleanLine(line))
+ s.description.WriteRune('\n')
+ }
+
+ if s.isDescription {
+ s.value.Description = strings.TrimSpace(s.description.String())
+ }
+ return s.value
+}
+
+func matchTag(line string) (match bool, name, kind, remain string) {
+ // If kind is specified
+ subMatches := tagRegex.FindStringSubmatch(line)
+ if len(subMatches) == 4 && subMatches[1] != "" {
+ return true, subMatches[1], subMatches[2], subMatches[3]
+ }
+ // If kind is not specified
+ subMatches = tagRegexNoKind.FindStringSubmatch(line)
+ if len(subMatches) == 3 && subMatches[1] != "" {
+ return true, subMatches[1], "", subMatches[2]
+ }
+ return false, "", "", ""
+}
+
+func matchDefaultTag(line string) (bool, string) {
+ subMatches := defaultRegex.FindStringSubmatch(line)
+ if len(subMatches) != 2 {
+ return false, ""
+ }
+ return true, subMatches[1]
+}
+
+func cleanLine(line string) string {
+ line2 := strings.TrimSpace(line)
+ if len(line2) < 3 {
+ return ""
+ }
+ if line2[0] != '#' {
+ slog.WarnContext(context.Background(), "Misformatted line", "line", line)
+ return ""
+ }
+ return line2[2:]
+}
+
+// getDefaultForValue takes a value detected from the comments, and looks up its
+// default value in the Helm chart.
+func getDefaultForValue(valueName string, chartValues map[string]interface{}) ([]byte, error) {
+ parts := strings.Split(valueName, ".")
+ // Check if this is a nested value
+ if len(parts) > 1 {
+ chartValue, ok := chartValues[parts[0]]
+ if !ok {
+ // Stop if the value is unknown
+ return nil, trace.NotFound("value '%s' not found", parts[0])
+ }
+
+ // The value name is part0.part1...partX
+ // We expect the detected Helm value to be a map, else we don't know
+ // how to access "part1...partX"
+ if subValue, ok := chartValue.(map[string]interface{}); ok {
+ return getDefaultForValue(strings.Join(parts[1:], "."), subValue)
+ }
+ return nil, trace.CompareFailed("value %s cannot be cast to a map", parts[0])
+
+ }
+ // If the value is known we marshall it as JSON
+ if chartValue, ok := chartValues[parts[0]]; ok {
+ return json.Marshal(chartValue)
+ }
+ return nil, trace.NotFound("value '%s' not found", parts[0])
+}
diff --git a/build.assets/tooling/cmd/render-helm-ref/template.go b/build.assets/tooling/lib/helm/template.go
similarity index 99%
rename from build.assets/tooling/cmd/render-helm-ref/template.go
rename to build.assets/tooling/lib/helm/template.go
index 10168526e2d..1bb3b43a972 100644
--- a/build.assets/tooling/cmd/render-helm-ref/template.go
+++ b/build.assets/tooling/lib/helm/template.go
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package main
+package helm
import (
"bytes"
diff --git a/build.assets/tooling/cmd/render-helm-ref/testdata/Chart.yaml b/build.assets/tooling/lib/helm/testdata/Chart.yaml
similarity index 100%
rename from build.assets/tooling/cmd/render-helm-ref/testdata/Chart.yaml
rename to build.assets/tooling/lib/helm/testdata/Chart.yaml
diff --git a/build.assets/tooling/cmd/render-helm-ref/testdata/expected-output.mdx b/build.assets/tooling/lib/helm/testdata/expected-output.mdx
similarity index 100%
rename from build.assets/tooling/cmd/render-helm-ref/testdata/expected-output.mdx
rename to build.assets/tooling/lib/helm/testdata/expected-output.mdx
diff --git a/build.assets/tooling/cmd/render-helm-ref/testdata/values.yaml b/build.assets/tooling/lib/helm/testdata/values.yaml
similarity index 100%
rename from build.assets/tooling/cmd/render-helm-ref/testdata/values.yaml
rename to build.assets/tooling/lib/helm/testdata/values.yaml
diff --git a/common.mk b/common.mk
index 9038263c888..319eb14542b 100644
--- a/common.mk
+++ b/common.mk
@@ -56,8 +56,12 @@ diag-bpf-vars:
# Dir of last included file, in this case common.mk:
COMMON_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
# This allows $(GOTESTSUM) in any Makefile that includes common.mk:
+# TOOLS_DIR contains external test tools
TOOLS_DIR := $(abspath $(COMMON_MK_DIR)/build.assets/tools)
GOTESTSUM = "$$( GOWORK=off go -C $(TOOLS_DIR)/gotestsum tool -n gotestsum )"
GCI = "$$( GOWORK=off go -C $(TOOLS_DIR)/gci tool -n gci )"
GODA = "$$( GOWORK=off go -C $(TOOLS_DIR)/goda tool -n goda )"
BENCHSTAT = "$$( GOWORK=off go -C $(TOOLS_DIR)/benchstat tool -n benchstat )"
+# TOOLING_DIR contains internal tooling
+TOOLING_DIR := $(abspath $(COMMON_MK_DIR)/build.assets/tooling)
+HELMJANITOR = "$$( GOWORK=off CGO_ENABLED=0 go -C $(TOOLING_DIR) tool -n helm-janitor )"
diff --git a/docs/pages/includes/helm-reference/zz_generated.access-mattermost.mdx b/docs/pages/includes/helm-reference/zz_generated.access-mattermost.mdx
index 8af5e5a5aaf..dac861bf7a3 100644
--- a/docs/pages/includes/helm-reference/zz_generated.access-mattermost.mdx
+++ b/docs/pages/includes/helm-reference/zz_generated.access-mattermost.mdx
@@ -73,7 +73,7 @@ You can pass the Mattermost token:
|------|---------|
| `string` | `""` |
-`mattermost.url` is the Jira URL. For example: `https://mattermost.example.com`.
+`mattermost.url` is the Mattermost URL. For example: `https://mattermost.example.com`.
### `mattermost.token`
diff --git a/examples/chart/Makefile b/examples/chart/Makefile
index c78a5cd71fb..9f1b0261361 100644
--- a/examples/chart/Makefile
+++ b/examples/chart/Makefile
@@ -1,122 +1,33 @@
-# TODO(hugoShaka): uncomment the additional targets as we start sync-ing
-# the reference and the values.yaml
-
-access = discord email jira mattermost msteams pagerduty slack datadog
-
-check_access = $(addprefix check-chart-ref-access-,$(access))
-render_access = $(addprefix render-chart-ref-access-,$(access))
+include ../../common.mk
.PHONY: render-chart-ref
-render-chart-ref: render-chart-ref-example render-chart-ref-teleport-operator render-chart-ref-teleport-kube-agent render-chart-ref-teleport-relay render-chart-ref-tbot render-chart-ref-tbot-spiffe-daemon-set $(render_access) render-chart-ref-event-handler # render-chart-ref-teleport-cluster
+render-chart-ref:
+ $(HELMJANITOR) reference --root-dir="../.."
-.PHONY: render-chart-ref-example
-render-chart-ref-example:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ./cmd/render-helm-ref/testdata -output ./cmd/render-helm-ref/testdata/expected-output.mdx
-
-# .PHONY: render-chart-ref-teleport-cluster
-# render-chart-ref-teleport-cluster:
-# cd ../../build.assets/tooling && \
-# go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-cluster/ -output ../../docs/pages/includes/helm-reference/zz_generated.teleport-cluster.mdx
-#
-#
-.PHONY: render-chart-ref-teleport-kube-agent
-render-chart-ref-teleport-kube-agent:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-kube-agent/ -output ../../docs/pages/includes/helm-reference/zz_generated.teleport-kube-agent.mdx
-
-.PHONY: render-chart-ref-teleport-relay
-render-chart-ref-teleport-relay:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-relay/ -output ../../docs/pages/includes/helm-reference/zz_generated.teleport-relay.mdx
-
-.PHONY: render-chart-ref-teleport-operator
-render-chart-ref-teleport-operator:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-cluster/charts/teleport-operator -output ../../docs/pages/includes/helm-reference/zz_generated.teleport-operator.mdx
-
-.PHONY: render-chart-ref-tbot
-render-chart-ref-tbot:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/tbot -output ../../docs/pages/includes/helm-reference/zz_generated.tbot.mdx
-
-.PHONY: render-chart-ref-tbot-spiffe-daemon-set
-render-chart-ref-tbot-spiffe-daemon-set:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/tbot-spiffe-daemon-set -output ../../docs/pages/includes/helm-reference/zz_generated.tbot-spiffe-daemon-set.mdx
-
-.PHONY: render-chart-ref-access-%
-render-chart-ref-access-%:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/access/$* -output ../../docs/pages/includes/helm-reference/zz_generated.access-$*.mdx
-
-.PHONY: render-chart-ref-event-handler
-render-chart-ref-event-handler:
- cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/event-handler -output ../../docs/pages/includes/helm-reference/zz_generated.event-handler.mdx
+.PHONY: render-chart-ref-%
+render-chart-ref-%:
+ $(HELMJANITOR) reference --charts=$* --root-dir="../.."
.PHONY: check-chart-ref
-check-chart-ref: check-chart-ref-example check-chart-ref-teleport-operator check-chart-ref-teleport-kube-agent check-chart-ref-teleport-relay check-chart-ref-tbot check-chart-ref-tbot-spiffe-daemon-set $(check_access) check-chart-ref-event-handler #check-chart-ref-teleport-cluster
+check-chart-ref:
+ $(HELMJANITOR) reference --root-dir="../.." --check
-.PHONY: check-chart-ref-example
-check-chart-ref-example:
- @ echo "Checking example chart reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ./cmd/render-helm-ref/testdata -output - | diff ../../build.assets/tooling/cmd/render-helm-ref/testdata/expected-output.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
+.PHONY: check-chart-ref-%
+check-chart-ref-%:
+ $(HELMJANITOR) reference --charts=$* --root-dir="../.." --check
-# .PHONY: check-chart-ref-teleport-cluster
-# check-chart-ref-teleport-cluster:
-# echo "Checking teleport-cluster reference"
-# cd ../../build.assets/tooling && \
-# go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-cluster -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.teleport-cluster.mdx - || \
-# ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
-#
-.PHONY: check-chart-ref-teleport-kube-agent
-check-chart-ref-teleport-kube-agent:
- @ echo "Checking teleport-kube-agent reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-kube-agent -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.teleport-kube-agent.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
+.PHONY: test
+test:
+ $(HELMJANITOR) test --root-dir="../.."
-.PHONY: check-chart-ref-teleport-relay
-check-chart-ref-teleport-relay:
- @ echo "Checking teleport-relay reference"
- @ cd ../../build.assets/tooling && \
- GOWORK=off go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-relay -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.teleport-relay.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
+.PHONY: test-%
+test-%:
+ $(HELMJANITOR) test --charts=$* --root-dir="../.."
-.PHONY: check-chart-ref-teleport-operator
-check-chart-ref-teleport-operator:
- @echo "Checking teleport-operator reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/teleport-cluster/charts/teleport-operator -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.teleport-operator.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
+.PHONY: lint
+lint:
+ $(HELMJANITOR) lint --root-dir="../.."
-.PHONY: check-chart-ref-tbot
-check-chart-ref-tbot:
- @echo "Checking tbot reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/tbot -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.tbot.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make render-chart-ref'" && exit 1 )
-
-.PHONY: check-chart-ref-tbot-spiffe-daemon-set
-check-chart-ref-tbot-spiffe-daemon-set:
- @echo "Checking tbot-spiffe-daemon-set reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/tbot-spiffe-daemon-set -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.tbot-spiffe-daemon-set.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make render-chart-ref'" && exit 1 )
-
-.PHONY: check-chart-ref-access-%
-check-chart-ref-access-%:
- @echo "Checking access/$* reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/access/$* -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.access-$*.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
-
-.PHONY: check-chart-ref-event-handler
-check-chart-ref-event-handler:
- @echo "Checking event-handler reference"
- @ cd ../../build.assets/tooling && \
- go run ./cmd/render-helm-ref -chart ../../examples/chart/event-handler -output - | diff ../../docs/pages/includes/helm-reference/zz_generated.event-handler.mdx - || \
- ( echo "Chart values.yaml and reference differ, please run 'make -C examples/chart render-chart-ref'" && exit 1 )
+.PHONY: lint-%
+lint-%:
+ $(HELMJANITOR) lint --charts=$* --root-dir="../.."
diff --git a/examples/chart/access/datadog/templates/configmap.yaml b/examples/chart/access/datadog/templates/configmap.yaml
index 418c1db7d49..1432de048ee 100644
--- a/examples/chart/access/datadog/templates/configmap.yaml
+++ b/examples/chart/access/datadog/templates/configmap.yaml
@@ -11,7 +11,7 @@ metadata:
data:
teleport-datadog.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/datadog/teleport-identity/{{ include "datadog.identitySecretPath" . }}"
refresh_identity = true
diff --git a/examples/chart/access/discord/templates/configmap.yaml b/examples/chart/access/discord/templates/configmap.yaml
index 9ce7d1d11b4..eb1e3afe98a 100644
--- a/examples/chart/access/discord/templates/configmap.yaml
+++ b/examples/chart/access/discord/templates/configmap.yaml
@@ -11,7 +11,7 @@ metadata:
data:
teleport-discord.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/discord/teleport-identity/{{ include "discord.identitySecretPath" . }}"
refresh_identity = true
diff --git a/examples/chart/access/email/templates/configmap.yaml b/examples/chart/access/email/templates/configmap.yaml
index ce263faa24d..75d77934f89 100644
--- a/examples/chart/access/email/templates/configmap.yaml
+++ b/examples/chart/access/email/templates/configmap.yaml
@@ -11,15 +11,17 @@ metadata:
data:
teleport-email.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/email/teleport-identity/{{ include "email.identitySecretPath" . }}"
refresh_identity = true
- {{ if .Values.mailgun.enabled -}}
+ {{- if .Values.mailgun.enabled }}
+
[mailgun]
domain = "{{ .Values.mailgun.domain }}"
private_key_file = "/var/lib/teleport/plugins/email/mailgun_private_key"
- {{ else if .Values.smtp.enabled -}}
+ {{- else if .Values.smtp.enabled }}
+
[smtp]
host = "{{ .Values.smtp.host }}"
port = {{ .Values.smtp.port }}
diff --git a/examples/chart/access/email/tests/__snapshot__/configmap_test.yaml.snap b/examples/chart/access/email/tests/__snapshot__/configmap_test.yaml.snap
index 33962fd77ac..b66711f5507 100644
--- a/examples/chart/access/email/tests/__snapshot__/configmap_test.yaml.snap
+++ b/examples/chart/access/email/tests/__snapshot__/configmap_test.yaml.snap
@@ -12,7 +12,6 @@ should match the snapshot (mailgun on):
domain = "mymailgunsubdomain.mailgun.org"
private_key_file = "/var/lib/teleport/plugins/email/mailgun_private_key"
-
[delivery]
sender = ""
recipients = []
diff --git a/examples/chart/access/jira/templates/configmap.yaml b/examples/chart/access/jira/templates/configmap.yaml
index 95d1c3d842d..7bf43cea2af 100644
--- a/examples/chart/access/jira/templates/configmap.yaml
+++ b/examples/chart/access/jira/templates/configmap.yaml
@@ -11,7 +11,7 @@ metadata:
data:
teleport-jira.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/jira/teleport-identity/{{ include "jira.identitySecretPath" . }}"
refresh_identity = true
@@ -30,7 +30,7 @@ data:
https_key_file = "/var/lib/teleport/plugins/jira/tls/tls.key"
https_cert_file = "/var/lib/teleport/plugins/jira/tls/tls.crt"
- {{ if .Values.http.basicAuth.enabled -}}
+ {{- if .Values.http.basicAuth.enabled }}
[http.basic_auth]
user = {{ .Values.http.basicAuth.user }}
password = {{ .Values.http.basicAuth.password }}
diff --git a/examples/chart/access/jira/templates/secret.yaml b/examples/chart/access/jira/templates/secret.yaml
index 76365c2e58a..b6a9060b066 100644
--- a/examples/chart/access/jira/templates/secret.yaml
+++ b/examples/chart/access/jira/templates/secret.yaml
@@ -8,6 +8,6 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
data:
- jiraApiToken: {{ .Values.jira.apiToken | b64enc }}
+ jiraApiToken: {{ default "" (.Values.jira.apiToken | b64enc) | quote}}
type: Opaque
{{- end }}
diff --git a/examples/chart/access/jira/tests/__snapshot__/configmap_test.yaml.snap b/examples/chart/access/jira/tests/__snapshot__/configmap_test.yaml.snap
index f158ec22500..d7324dfc53d 100644
--- a/examples/chart/access/jira/tests/__snapshot__/configmap_test.yaml.snap
+++ b/examples/chart/access/jira/tests/__snapshot__/configmap_test.yaml.snap
@@ -21,8 +21,6 @@ should match the snapshot (smtp on):
https_key_file = "/var/lib/teleport/plugins/jira/tls/tls.key"
https_cert_file = "/var/lib/teleport/plugins/jira/tls/tls.crt"
-
-
[log]
output = "/var/log/teleport-jira.log"
severity = "DEBUG"
diff --git a/examples/chart/access/mattermost/templates/configmap.yaml b/examples/chart/access/mattermost/templates/configmap.yaml
index 95ea14e9ad8..7c0367f203c 100644
--- a/examples/chart/access/mattermost/templates/configmap.yaml
+++ b/examples/chart/access/mattermost/templates/configmap.yaml
@@ -11,7 +11,7 @@ metadata:
data:
teleport-mattermost.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/mattermost/teleport-identity/{{ include "mattermost.identitySecretPath" . }}"
refresh_identity = true
diff --git a/examples/chart/access/mattermost/values.yaml b/examples/chart/access/mattermost/values.yaml
index 552e9eaefaf..691d72c84d1 100644
--- a/examples/chart/access/mattermost/values.yaml
+++ b/examples/chart/access/mattermost/values.yaml
@@ -47,7 +47,7 @@ teleport:
# - via the chart Values by setting `mattermost.token`
# - via an existing Kubernetes Secret by setting `mattermost.tokenFromSecret`
mattermost:
- # mattermost.url(string) -- is the Jira URL. For example: `https://mattermost.example.com`.
+ # mattermost.url(string) -- is the Mattermost URL. For example: `https://mattermost.example.com`.
url: ""
# mattermost.token(string) -- is the Mattermost token used by the plugin to interact
# with Mattermost. When set, the Chart creates a Kubernetes Secret for you.
diff --git a/examples/chart/access/msteams/templates/configmap.yaml b/examples/chart/access/msteams/templates/configmap.yaml
index e54bc5e6861..a913efac228 100644
--- a/examples/chart/access/msteams/templates/configmap.yaml
+++ b/examples/chart/access/msteams/templates/configmap.yaml
@@ -11,9 +11,9 @@ metadata:
data:
teleport-msteams.toml: |
preload = true
-
+
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/msteams/teleport-identity/{{ include "msteams.identitySecretPath" . }}"
refresh_identity = true
diff --git a/examples/chart/access/pagerduty/templates/configmap.yaml b/examples/chart/access/pagerduty/templates/configmap.yaml
index 44d2808ac13..aa9396ddebc 100644
--- a/examples/chart/access/pagerduty/templates/configmap.yaml
+++ b/examples/chart/access/pagerduty/templates/configmap.yaml
@@ -11,7 +11,7 @@ metadata:
data:
teleport-pagerduty.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/pagerduty/teleport-identity/{{ include "pagerduty.identitySecretPath" . }}"
refresh_identity = true
diff --git a/examples/chart/access/slack/templates/configmap.yaml b/examples/chart/access/slack/templates/configmap.yaml
index 829135c52bc..f35ad9c5846 100644
--- a/examples/chart/access/slack/templates/configmap.yaml
+++ b/examples/chart/access/slack/templates/configmap.yaml
@@ -11,7 +11,7 @@ metadata:
data:
teleport-slack.toml: |
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/slack/teleport-identity/{{ include "slack.identitySecretPath" . }}"
refresh_identity = true
diff --git a/examples/chart/event-handler/templates/configmap.yaml b/examples/chart/event-handler/templates/configmap.yaml
index c2feef57525..e8f94728931 100644
--- a/examples/chart/event-handler/templates/configmap.yaml
+++ b/examples/chart/event-handler/templates/configmap.yaml
@@ -52,7 +52,7 @@ data:
{{- end }}
[teleport]
- addr = {{ coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress | quote }}
+ addr = {{ default "" (coalesce .Values.teleport.address .Values.tbot.teleportProxyAddress .Values.tbot.teleportAuthAddress) | quote }}
identity = "/var/lib/teleport/plugins/event-handler/teleport-identity/{{ include "event-handler.identitySecretPath" . }}"
refresh.enabled = true
diff --git a/version.mk b/version.mk
index cd02ec5ef72..63d1dcb4138 100644
--- a/version.mk
+++ b/version.mk
@@ -1,3 +1,6 @@
+# Include common makefile shared between OSS and Ent.
+include common.mk
+
GITREF=$(shell git describe --long --tags)
# $(GITREF_GO) will be written to gitref.go
@@ -17,14 +20,10 @@ setver: validate-semver helm-version tsh-version
# so that chart versions are also kept in sync when the Teleport version is updated for a release.
# If the version contains '-dev' (as it does on the master branch, or for development builds) then we get the latest
# published major version number by parsing a sorted list of git tags instead, to make deploying the chart from master
-# work as expected. Version numbers are quoted as a string because Helm otherwise treats dotted decimals as floats.
-# The weird -i usage is to make the sed commands work the same on both Linux and Mac. Test on both platforms if you change it.
+# work as expected.
.PHONY:helm-version
helm-version:
- for CHART in teleport-cluster tbot tbot-spiffe-daemon-set teleport-kube-updater teleport-kube-agent teleport-cluster/charts/teleport-operator teleport-relay event-handler access/discord access/email access/jira access/mattermost access/msteams access/pagerduty access/slack access/datadog; do \
- sed -i'.bak' -e "s_^\\.version:\ .*_.version: \\&version \"$${VERSION}\"_g" examples/chart/$${CHART}/Chart.yaml || exit 1; \
- rm -f examples/chart/$${CHART}/Chart.yaml.bak; \
- done
+ $(HELMJANITOR) update-version $(VERSION)
TSH_APP_PLISTS := $(wildcard build.assets/macos/*/tsh.app/Contents/Info.plist)
PLIST_FILES := $(abspath $(TSH_APP_PLISTS))