Merge pull request #8718 from dokku/8717-scheduler-k3s-charts-set-cannot-persist-property-names-containing-and-silently-breaks-multi-line-values

This commit is contained in:
Jose Diaz-Gonzalez
2026-05-31 03:40:52 -04:00
committed by GitHub
12 changed files with 497 additions and 23 deletions

View File

@@ -716,6 +716,8 @@ dokku scheduler-k3s:charts:set cert-manager.version 1.13.3
> [!NOTE]
> Properties follow dot-notation, and are expanded according to Helm's internal logic. See the [Helm documentation](https://helm.sh/docs/helm/helm_install/#helm-install) for `helm install` for further details.
Property names may contain `/` (e.g. for Kubernetes-style annotation keys such as `service.annotations.prometheus.io/scrape`) and values may span multiple lines; both are preserved verbatim.
To unset a chart property, omit the value from the `scheduler-k3s:charts:set` call:
```shell

View File

@@ -2,6 +2,7 @@ package common
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"os"
@@ -514,6 +515,99 @@ func PropertyWrite(pluginName string, appName string, property string, value str
return nil
}
// PropertyMapWrite persists a string→string map for a given property as a single
// JSON file. Unlike PropertyWrite/PropertyListWrite, map *keys* may contain any
// bytes (including '/' or '\n') because they live inside file content rather
// than in the on-disk filename. A nil map is persisted as "{}".
func PropertyMapWrite(pluginName string, appName string, property string, m map[string]string) error {
if err := propertyTouch(pluginName, appName, property); err != nil {
return err
}
if m == nil {
m = map[string]string{}
}
b, err := json.Marshal(m)
if err != nil {
return fmt.Errorf("Unable to marshal %s map property %s.%s: %w", pluginName, appName, property, err)
}
propertyPath := getPropertyPath(pluginName, appName, property)
if err := os.WriteFile(propertyPath, b, 0600); err != nil {
return fmt.Errorf("Unable to write %s map property %s.%s: %w", pluginName, appName, property, err)
}
if err := SetPermissions(SetPermissionInput{
Filename: propertyPath,
Mode: os.FileMode(0600),
}); err != nil {
return fmt.Errorf("Unable to set permissions for %s map property %s.%s: %w", pluginName, appName, property, err)
}
return nil
}
// PropertyMapGet returns the string→string map persisted for a property. A
// missing or empty-file property is reported as an empty map without error.
func PropertyMapGet(pluginName string, appName string, property string) (map[string]string, error) {
m := map[string]string{}
if !PropertyExists(pluginName, appName, property) {
return m, nil
}
b, err := os.ReadFile(getPropertyPath(pluginName, appName, property))
if err != nil {
return nil, fmt.Errorf("Unable to read %s map property %s.%s: %w", pluginName, appName, property, err)
}
if len(b) == 0 {
return m, nil
}
if err := json.Unmarshal(b, &m); err != nil {
return nil, fmt.Errorf("Unable to parse %s map property %s.%s as JSON: %w", pluginName, appName, property, err)
}
return m, nil
}
// PropertyMapSet reads the current map, sets one key, and writes the map back.
func PropertyMapSet(pluginName string, appName string, property string, key string, value string) error {
m, err := PropertyMapGet(pluginName, appName, property)
if err != nil {
return err
}
m[key] = value
return PropertyMapWrite(pluginName, appName, property, m)
}
// PropertyMapDelete reads the current map, removes one key, and writes the map
// back. Removing a key that is not present is a no-op.
func PropertyMapDelete(pluginName string, appName string, property string, key string) error {
if !PropertyExists(pluginName, appName, property) {
return nil
}
m, err := PropertyMapGet(pluginName, appName, property)
if err != nil {
return err
}
if _, ok := m[key]; !ok {
return nil
}
delete(m, key)
return PropertyMapWrite(pluginName, appName, property, m)
}
// PropertyMapLength returns the number of entries in the map persisted for a
// property. A missing property is reported as length 0 without error.
func PropertyMapLength(pluginName string, appName string, property string) (int, error) {
m, err := PropertyMapGet(pluginName, appName, property)
if err != nil {
return 0, err
}
return len(m), nil
}
// PropertySetup creates the plugin config root
func PropertySetup(pluginName string) error {
configRoot := filepath.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config")

View File

@@ -0,0 +1,170 @@
package common
import (
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
)
const (
propertyTestPlugin = "test-plugin"
propertyTestApp = "--global"
propertyTestName = "chart-overrides.traefik"
)
func setupPropertyMapTest(t *testing.T) {
t.Helper()
RegisterTestingT(t)
Expect(setupTests()).To(Succeed())
t.Setenv("DOKKU_LIB_ROOT", t.TempDir())
t.Setenv("DOKKU_SYSTEM_USER", "root")
t.Setenv("DOKKU_SYSTEM_GROUP", "root")
Expect(PropertySetup(propertyTestPlugin)).To(Succeed())
}
func TestPropertyMapWriteAndGetRoundTrip(t *testing.T) {
setupPropertyMapTest(t)
want := map[string]string{"installCRDs": "false", "version": "1.13.3"}
Expect(PropertyMapWrite(propertyTestPlugin, propertyTestApp, propertyTestName, want)).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(want))
}
func TestPropertyMapHandlesSlashInKey(t *testing.T) {
setupPropertyMapTest(t)
key := "service.annotations.prometheus.io/scrape"
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, key, "true")).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(HaveKeyWithValue(key, "true"))
}
func TestPropertyMapHandlesNewlineInValue(t *testing.T) {
setupPropertyMapTest(t)
value := "line one\nline two\nline three"
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "controller.config", value)).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(HaveKeyWithValue("controller.config", value))
}
func TestPropertyMapEmptyMap(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapWrite(propertyTestPlugin, propertyTestApp, propertyTestName, map[string]string{})).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeEmpty())
}
func TestPropertyMapNilMapPersistsAsEmptyJSON(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapWrite(propertyTestPlugin, propertyTestApp, propertyTestName, nil)).To(Succeed())
b, err := os.ReadFile(getPropertyPath(propertyTestPlugin, propertyTestApp, propertyTestName))
Expect(err).NotTo(HaveOccurred())
Expect(string(b)).To(Equal("{}"))
}
func TestPropertyMapGetMissingReturnsEmpty(t *testing.T) {
setupPropertyMapTest(t)
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, "chart-overrides.never-written")
Expect(err).NotTo(HaveOccurred())
Expect(got).To(BeEmpty())
}
func TestPropertyMapSetCreatesMissingFile(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyExists(propertyTestPlugin, propertyTestApp, propertyTestName)).To(BeFalse())
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "version", "1.0")).To(Succeed())
Expect(PropertyExists(propertyTestPlugin, propertyTestApp, propertyTestName)).To(BeTrue())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(map[string]string{"version": "1.0"}))
}
func TestPropertyMapDeleteRemovesKeyOnly(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "version", "1.0")).To(Succeed())
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "installCRDs", "false")).To(Succeed())
Expect(PropertyMapDelete(propertyTestPlugin, propertyTestApp, propertyTestName, "version")).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(map[string]string{"installCRDs": "false"}))
}
func TestPropertyMapDeleteMissingKeyIsNoOp(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "version", "1.0")).To(Succeed())
Expect(PropertyMapDelete(propertyTestPlugin, propertyTestApp, propertyTestName, "missing")).To(Succeed())
Expect(PropertyMapDelete(propertyTestPlugin, propertyTestApp, "chart-overrides.never-written", "anything")).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(map[string]string{"version": "1.0"}))
}
func TestPropertyMapLength(t *testing.T) {
setupPropertyMapTest(t)
length, err := PropertyMapLength(propertyTestPlugin, propertyTestApp, "chart-overrides.never-written")
Expect(err).NotTo(HaveOccurred())
Expect(length).To(Equal(0))
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "a", "1")).To(Succeed())
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "b", "2")).To(Succeed())
length, err = PropertyMapLength(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(length).To(Equal(2))
}
func TestPropertyMapRewriteIsStable(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "version", "1.0")).To(Succeed())
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "version", "1.0")).To(Succeed())
got, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(map[string]string{"version": "1.0"}))
}
func TestPropertyMapFileMode(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapSet(propertyTestPlugin, propertyTestApp, propertyTestName, "version", "1.0")).To(Succeed())
fi, err := os.Stat(filepath.Clean(getPropertyPath(propertyTestPlugin, propertyTestApp, propertyTestName)))
Expect(err).NotTo(HaveOccurred())
Expect(fi.Mode().Perm()).To(Equal(os.FileMode(0600)))
}
func TestPropertyMapMalformedJSONReturnsError(t *testing.T) {
setupPropertyMapTest(t)
Expect(PropertyMapWrite(propertyTestPlugin, propertyTestApp, propertyTestName, map[string]string{"k": "v"})).To(Succeed())
Expect(os.WriteFile(getPropertyPath(propertyTestPlugin, propertyTestApp, propertyTestName), []byte("not-json"), 0600)).To(Succeed())
_, err := PropertyMapGet(propertyTestPlugin, propertyTestApp, propertyTestName)
Expect(err).To(HaveOccurred())
}

View File

@@ -25,7 +25,11 @@ func setupTests() (err error) {
return err
}
return os.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled")
if err := os.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled"); err != nil {
return err
}
return os.Setenv("PLUGIN_CORE_AVAILABLE_PATH", "/var/lib/dokku/core-plugins/available")
}
func setupTestApp() (err error) {

View File

@@ -1774,12 +1774,11 @@ func installHelmCharts(ctx context.Context, clientset KubernetesClient, shouldIn
values = updateVectorValues(values)
}
chartProperties, err := common.PropertyGetAllByPrefix("scheduler-k3s", "--global", "chart."+chart.ReleaseName+".")
chartProperties, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides."+chart.ReleaseName)
if err != nil {
return fmt.Errorf("Error getting chart properties: %w", err)
}
for key, value := range chartProperties {
key = strings.TrimPrefix(key, "chart."+chart.ReleaseName+".")
strval := fmt.Sprintf("%s=%s", key, value)
if err := strvals.ParseInto(strval, values); err != nil {
return fmt.Errorf("Error parsing chart property %s: %w", strval, err)

View File

@@ -105,14 +105,17 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
}
}
chartProperties, err := common.PropertyGetAllByPrefix("scheduler-k3s", "--global", "chart.")
if err != nil {
return fmt.Errorf("Unable to get property list: %w", err)
}
for name, value := range chartProperties {
flagName := "--scheduler-k3s-global-" + name
flags[flagName] = func(appName string) string {
return value
for _, chart := range HelmCharts {
chartOverrides, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides."+chart.ReleaseName)
if err != nil {
return fmt.Errorf("Unable to get property list: %w", err)
}
for key, value := range chartOverrides {
flagName := fmt.Sprintf("--scheduler-k3s-global-chart.%s.%s", chart.ReleaseName, key)
value := value
flags[flagName] = func(appName string) string {
return value
}
}
}

View File

@@ -95,6 +95,7 @@ var LabelResourceTypes = []string{
// exclude such properties when scanning the property store.
var reservedAnnotationPrefixes = []string{
"chart.",
"chart-overrides.",
"labels.",
"node-profile-",
TriggerAuthPropertyPrefix,

View File

@@ -471,16 +471,17 @@ func CommandChartsSet(propertyArg string, value string) error {
}
key := fmt.Sprintf("chart.%s.%s", chartName, chartProperty)
mapProperty := "chart-overrides." + chartName
if value != "" {
common.LogInfo2Quiet(fmt.Sprintf("Setting %s to %s", key, value))
if err := common.PropertyWrite("scheduler-k3s", "--global", key, value); err != nil {
if err := common.PropertyMapSet("scheduler-k3s", "--global", mapProperty, chartProperty, value); err != nil {
return fmt.Errorf("Unable to write property: %w", err)
}
return nil
}
common.LogInfo2Quiet(fmt.Sprintf("Unsetting %s", key))
if err := common.PropertyDelete("scheduler-k3s", "--global", key); err != nil {
if err := common.PropertyMapDelete("scheduler-k3s", "--global", mapProperty, chartProperty); err != nil {
return fmt.Errorf("Unable to delete property: %w", err)
}
return nil
@@ -522,16 +523,12 @@ func CommandChartsReport(chartName string, format string, infoFlag string) error
flatOverrides := map[string]string{}
flagToValue := map[string]string{}
for _, chart := range charts {
properties, err := common.PropertyGetAllByPrefix("scheduler-k3s", "--global", "chart."+chart.ReleaseName+".")
chartOverrides, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides."+chart.ReleaseName)
if err != nil {
return fmt.Errorf("Unable to get chart properties: %w", err)
}
chartOverrides := map[string]string{}
prefix := "chart." + chart.ReleaseName + "."
for key, value := range properties {
overrideKey := strings.TrimPrefix(key, prefix)
chartOverrides[overrideKey] = value
for overrideKey, value := range chartOverrides {
flatKey := chart.ReleaseName + "." + overrideKey
flatOverrides[flatKey] = value
flagToValue["--scheduler-k3s-charts-"+flatKey] = value
@@ -1372,17 +1369,32 @@ func CommandSet(appName string, property string, value string) error {
}
chartName := chartParts[1]
chartProperty := chartParts[2]
chartFound := false
for _, chart := range HelmCharts {
if chart.ReleaseName == chartName {
validProperties[property] = ""
globalProperties[property] = true
chartFound = true
break
}
}
if _, ok := validProperties[property]; !ok {
if !chartFound {
return fmt.Errorf("Invalid chart property, no matching chart found: %s", property)
}
mapProperty := "chart-overrides." + chartName
if value != "" {
common.LogInfo2Quiet(fmt.Sprintf("Setting %s to %s", property, value))
if err := common.PropertyMapSet("scheduler-k3s", "--global", mapProperty, chartProperty, value); err != nil {
return fmt.Errorf("Unable to write property: %w", err)
}
return nil
}
common.LogInfo2Quiet(fmt.Sprintf("Unsetting %s", property))
if err := common.PropertyMapDelete("scheduler-k3s", "--global", mapProperty, chartProperty); err != nil {
return fmt.Errorf("Unable to delete property: %w", err)
}
return nil
}
common.CommandPropertySet("scheduler-k3s", appName, property, value, validProperties, globalProperties)

View File

@@ -75,6 +75,10 @@ func TriggerInstall() error {
return err
}
if err := migrateChartPropertiesToMapFormat(); err != nil {
return fmt.Errorf("Unable to migrate chart properties: %w", err)
}
if err := syncExistingCertificates(); err != nil {
common.LogWarn(fmt.Sprintf("Warning: failed to sync existing certificates: %v", err))
}
@@ -82,6 +86,46 @@ func TriggerInstall() error {
return nil
}
// migrateChartPropertiesToMapFormat moves legacy per-key chart.<chart>.<key>
// property files into a single chart-overrides.<chart> JSON map per chart.
// Without this migration, a dokku upgrade would silently strand any existing
// chart overrides because the new render path only reads chart-overrides.*.
// Idempotent: once the legacy files are gone the helper short-circuits per
// chart, and legacy values take precedence over any partial pre-existing
// entries on re-runs.
func migrateChartPropertiesToMapFormat() error {
for _, chart := range HelmCharts {
prefix := "chart." + chart.ReleaseName + "."
legacy, err := common.PropertyGetAllByPrefix("scheduler-k3s", "--global", prefix)
if err != nil {
return fmt.Errorf("Unable to read legacy chart properties for %s: %w", chart.ReleaseName, err)
}
if len(legacy) == 0 {
continue
}
mapProperty := "chart-overrides." + chart.ReleaseName
merged, err := common.PropertyMapGet("scheduler-k3s", "--global", mapProperty)
if err != nil {
return fmt.Errorf("Unable to read existing chart-overrides for %s: %w", chart.ReleaseName, err)
}
for fullKey, value := range legacy {
merged[strings.TrimPrefix(fullKey, prefix)] = value
}
if err := common.PropertyMapWrite("scheduler-k3s", "--global", mapProperty, merged); err != nil {
return fmt.Errorf("Unable to write chart-overrides for %s: %w", chart.ReleaseName, err)
}
for fullKey := range legacy {
if err := common.PropertyDelete("scheduler-k3s", "--global", fullKey); err != nil {
return fmt.Errorf("Unable to remove legacy chart property %s: %w", fullKey, err)
}
}
}
return nil
}
// TriggerPostCertsUpdate handles post-certs-update trigger
func TriggerPostCertsUpdate(appName string) error {
scheduler := common.PropertyGetDefault("scheduler", appName, "selected", "")

View File

@@ -0,0 +1,88 @@
package scheduler_k3s
import (
"testing"
"github.com/dokku/dokku/plugins/common"
. "github.com/onsi/gomega"
)
func setupChartMigrationTest(t *testing.T) {
t.Helper()
RegisterTestingT(t)
t.Setenv("PLUGIN_PATH", "/var/lib/dokku/plugins")
t.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled")
t.Setenv("DOKKU_LIB_ROOT", t.TempDir())
t.Setenv("DOKKU_SYSTEM_USER", "root")
t.Setenv("DOKKU_SYSTEM_GROUP", "root")
Expect(common.PropertySetup("scheduler-k3s")).To(Succeed())
}
func TestMigrateChartPropertiesToMapFormat(t *testing.T) {
setupChartMigrationTest(t)
Expect(common.PropertyWrite("scheduler-k3s", "--global", "chart.cert-manager.installCRDs", "false")).To(Succeed())
Expect(common.PropertyWrite("scheduler-k3s", "--global", "chart.cert-manager.version", "1.13.3")).To(Succeed())
Expect(common.PropertyWrite("scheduler-k3s", "--global", "chart.traefik.replicas", "2")).To(Succeed())
Expect(migrateChartPropertiesToMapFormat()).To(Succeed())
certManager, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides.cert-manager")
Expect(err).NotTo(HaveOccurred())
Expect(certManager).To(Equal(map[string]string{
"installCRDs": "false",
"version": "1.13.3",
}))
traefik, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides.traefik")
Expect(err).NotTo(HaveOccurred())
Expect(traefik).To(Equal(map[string]string{"replicas": "2"}))
Expect(common.PropertyExists("scheduler-k3s", "--global", "chart.cert-manager.installCRDs")).To(BeFalse())
Expect(common.PropertyExists("scheduler-k3s", "--global", "chart.cert-manager.version")).To(BeFalse())
Expect(common.PropertyExists("scheduler-k3s", "--global", "chart.traefik.replicas")).To(BeFalse())
}
func TestMigrateChartPropertiesToMapFormatIsIdempotent(t *testing.T) {
setupChartMigrationTest(t)
Expect(common.PropertyWrite("scheduler-k3s", "--global", "chart.cert-manager.version", "1.13.3")).To(Succeed())
Expect(migrateChartPropertiesToMapFormat()).To(Succeed())
Expect(migrateChartPropertiesToMapFormat()).To(Succeed())
got, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides.cert-manager")
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(map[string]string{"version": "1.13.3"}))
}
func TestMigrateChartPropertiesToMapFormatPreservesExistingNonOverlappingKeys(t *testing.T) {
setupChartMigrationTest(t)
Expect(common.PropertyMapWrite("scheduler-k3s", "--global", "chart-overrides.cert-manager", map[string]string{
"installCRDs": "true",
"service.annotations.prometheus.io/scrape": "true",
})).To(Succeed())
Expect(common.PropertyWrite("scheduler-k3s", "--global", "chart.cert-manager.installCRDs", "false")).To(Succeed())
Expect(common.PropertyWrite("scheduler-k3s", "--global", "chart.cert-manager.version", "1.13.3")).To(Succeed())
Expect(migrateChartPropertiesToMapFormat()).To(Succeed())
got, err := common.PropertyMapGet("scheduler-k3s", "--global", "chart-overrides.cert-manager")
Expect(err).NotTo(HaveOccurred())
Expect(got).To(Equal(map[string]string{
"installCRDs": "false",
"service.annotations.prometheus.io/scrape": "true",
"version": "1.13.3",
}))
}
func TestMigrateChartPropertiesToMapFormatNoLegacyIsNoOp(t *testing.T) {
setupChartMigrationTest(t)
Expect(migrateChartPropertiesToMapFormat()).To(Succeed())
for _, chart := range HelmCharts {
Expect(common.PropertyExists("scheduler-k3s", "--global", "chart-overrides."+chart.ReleaseName)).To(BeFalse())
}
}

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bats
load test_helper
setup() {
global_setup
}
teardown() {
dokku scheduler-k3s:charts:set traefik.service.annotations.prometheus.io/scrape >/dev/null 2>/dev/null || true
dokku scheduler-k3s:charts:set traefik.controller.config >/dev/null 2>/dev/null || true
dokku scheduler-k3s:charts:set cert-manager.installCRDs >/dev/null 2>/dev/null || true
global_teardown
}
@test "(scheduler-k3s:charts:set) accepts property names containing /" {
run /bin/bash -c "dokku scheduler-k3s:charts:set traefik.service.annotations.prometheus.io/scrape true"
assert_success
run /bin/bash -c "dokku scheduler-k3s:charts:report traefik --format json | jq -r '.\"traefik.service.annotations.prometheus.io/scrape\"'"
assert_success
assert_output "true"
run /bin/bash -c "dokku scheduler-k3s:charts:set traefik.service.annotations.prometheus.io/scrape"
assert_success
run /bin/bash -c "dokku scheduler-k3s:charts:report traefik --format json"
assert_success
assert_output "{}"
}
@test "(scheduler-k3s:charts:set) preserves multi-line values" {
local value=$'line one\nline two\nline three'
run /bin/bash -c "dokku scheduler-k3s:charts:set traefik.controller.config \"$value\""
assert_success
run /bin/bash -c "dokku scheduler-k3s:charts:report traefik --format json | jq -r '.\"traefik.controller.config\"'"
assert_success
assert_output "$value"
}
@test "(scheduler-k3s:set) deprecated chart.* form writes through map storage" {
run /bin/bash -c "dokku scheduler-k3s:set --global chart.cert-manager.installCRDs false"
assert_success
assert_output_contains "deprecated"
run /bin/bash -c "dokku scheduler-k3s:charts:report cert-manager --format json | jq -r '.\"cert-manager.installCRDs\"'"
assert_success
assert_output "false"
run /bin/bash -c "dokku scheduler-k3s:set --global chart.cert-manager.installCRDs"
assert_success
run /bin/bash -c "dokku scheduler-k3s:charts:report cert-manager --format json"
assert_success
assert_output "{}"
}