Merge pull request #8934 from dokku/8928-traefik-report-does-not-surface-the-dns-provider-properties

Report traefik dns-provider env vars as global keys
This commit is contained in:
Jose Diaz-Gonzalez
2026-08-12 04:52:58 -04:00
committed by GitHub
7 changed files with 275 additions and 24 deletions

View File

@@ -8,6 +8,9 @@ import (
"github.com/dokku/dokku/plugins/common"
)
// valueMask is shown in place of credential values in the default stdout report
const valueMask = "*******"
// ReportSingleApp is an internal function that displays the traefik report for one or more apps
func ReportSingleApp(appName string, format string, infoFlag string) error {
if appName != "--global" {
@@ -49,20 +52,16 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
"--traefik-global-log-level": reportGlobalLogLevel,
}
// dns-provider-* env vars are dynamic; their values are masked in the default
// stdout report, but shown for --format json or when queried explicitly by name
dnsProviderVars, err := common.PropertyGetAllByPrefix("traefik", "--global", "dns-provider-")
for _, flagName := range []string{"--traefik-computed-basic-auth-password", "--traefik-global-basic-auth-password"} {
flags[flagName] = maskedReportFunc(flags[flagName], flagName, format, infoFlag)
}
dnsProviderFlags, err := dnsProviderReportFlags(format, infoFlag)
if err != nil {
return err
}
for key, value := range dnsProviderVars {
flagName := "--traefik-" + key
realValue := value
if format == "json" || infoFlag == flagName {
flags[flagName] = func(string) string { return realValue }
} else {
flags[flagName] = func(string) string { return "*******" }
}
for flagName, reportFunc := range dnsProviderFlags {
flags[flagName] = reportFunc
}
flagKeys := []string{}
@@ -84,6 +83,40 @@ func ReportSingleApp(appName string, format string, infoFlag string) error {
})
}
// maskedReportFunc hides a credential value behind valueMask so the default stdout
// report - and thus the aggregate `dokku report` - never prints it. The raw value is
// returned for machine-readable output or when the flag is requested by name.
func maskedReportFunc(fn common.ReportFunc, flagName string, format string, infoFlag string) common.ReportFunc {
if format == "json" || infoFlag == flagName {
return fn
}
return func(appName string) string {
if fn(appName) == "" {
return ""
}
return valueMask
}
}
// dnsProviderReportFlags returns report functions for the dynamic dns-provider-*
// properties, keyed by their global report flag
func dnsProviderReportFlags(format string, infoFlag string) (map[string]common.ReportFunc, error) {
flags := map[string]common.ReportFunc{}
properties, err := common.PropertyGetAllByPrefix("traefik", "--global", "dns-provider-")
if err != nil {
return flags, err
}
for property, value := range properties {
flagName := "--traefik-global-" + property
flags[flagName] = maskedReportFunc(func(string) string { return value }, flagName, format, infoFlag)
}
return flags, nil
}
func reportGlobalAPIEnabled(appName string) string {
return common.PropertyGet("traefik", "--global", "api-enabled")
}

View File

@@ -0,0 +1,143 @@
package traefikvhosts
import (
"os/user"
"path/filepath"
"testing"
"github.com/dokku/dokku/plugins/common"
)
// setupPropertyEnv points the dokku env at temporary directories and tells the
// permission helpers to chown files to the current user (a no-op) so the test
// works without root.
func setupPropertyEnv(t *testing.T) {
t.Helper()
libRoot := t.TempDir()
t.Setenv("DOKKU_LIB_ROOT", libRoot)
t.Setenv("DOKKU_ROOT", t.TempDir())
t.Setenv("PLUGIN_PATH", filepath.Join(libRoot, "plugins"))
current, err := user.Current()
if err != nil {
t.Fatalf("user.Current: %v", err)
}
group, err := user.LookupGroupId(current.Gid)
if err != nil {
t.Fatalf("user.LookupGroupId: %v", err)
}
t.Setenv("DOKKU_SYSTEM_USER", current.Username)
t.Setenv("DOKKU_SYSTEM_GROUP", group.Name)
}
func staticReportFunc(value string) common.ReportFunc {
return func(string) string {
return value
}
}
func TestMaskedReportFunc(t *testing.T) {
const flagName = "--traefik-global-basic-auth-password"
cases := []struct {
name string
value string
format string
infoFlag string
want string
}{
{name: "stdout masks a set value", value: "hunter2", format: "stdout", want: valueMask},
{name: "empty format masks a set value", value: "hunter2", want: valueMask},
{name: "stdout leaves an unset value empty", value: "", format: "stdout", want: ""},
{name: "json returns the raw value", value: "hunter2", format: "json", want: "hunter2"},
{name: "matching info flag returns the raw value", value: "hunter2", format: "stdout", infoFlag: flagName, want: "hunter2"},
{name: "other info flag masks the value", value: "hunter2", format: "stdout", infoFlag: "--traefik-global-basic-auth-username", want: valueMask},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fn := maskedReportFunc(staticReportFunc(tc.value), flagName, tc.format, tc.infoFlag)
if got := fn("--global"); got != tc.want {
t.Errorf("maskedReportFunc(%q, %q, %q) = %q, want %q", tc.value, tc.format, tc.infoFlag, got, tc.want)
}
})
}
}
func TestDNSProviderReportFlags(t *testing.T) {
setupPropertyEnv(t)
if err := common.PropertyWrite("traefik", "--global", "dns-provider", "cloudflare"); err != nil {
t.Fatalf("PropertyWrite dns-provider: %v", err)
}
if err := common.PropertyWrite("traefik", "--global", "dns-provider-cf_api_email", "test@example.com"); err != nil {
t.Fatalf("PropertyWrite dns-provider-cf_api_email: %v", err)
}
if err := common.PropertyWrite("traefik", "--global", "dns-provider-cf_api_key", "secret-key"); err != nil {
t.Fatalf("PropertyWrite dns-provider-cf_api_key: %v", err)
}
flags, err := dnsProviderReportFlags("stdout", "")
if err != nil {
t.Fatalf("dnsProviderReportFlags: %v", err)
}
assertReportFuncValue(t, flags, "--traefik-global-dns-provider-cf_api_email", valueMask)
assertReportFuncValue(t, flags, "--traefik-global-dns-provider-cf_api_key", valueMask)
if len(flags) != 2 {
t.Errorf("flags = %v, want only the two dns-provider-* env var flags", flagNames(flags))
}
if _, ok := flags["--traefik-global-dns-provider"]; ok {
t.Error("--traefik-global-dns-provider should be reported statically, not as a dynamic env var")
}
if _, ok := flags["--traefik-dns-provider-cf_api_email"]; ok {
t.Error("--traefik-dns-provider-cf_api_email should not be emitted outside the global namespace")
}
flags, err = dnsProviderReportFlags("json", "")
if err != nil {
t.Fatalf("dnsProviderReportFlags: %v", err)
}
assertReportFuncValue(t, flags, "--traefik-global-dns-provider-cf_api_email", "test@example.com")
assertReportFuncValue(t, flags, "--traefik-global-dns-provider-cf_api_key", "secret-key")
flags, err = dnsProviderReportFlags("stdout", "--traefik-global-dns-provider-cf_api_key")
if err != nil {
t.Fatalf("dnsProviderReportFlags: %v", err)
}
assertReportFuncValue(t, flags, "--traefik-global-dns-provider-cf_api_email", valueMask)
assertReportFuncValue(t, flags, "--traefik-global-dns-provider-cf_api_key", "secret-key")
}
func TestDNSProviderReportFlagsWithoutProperties(t *testing.T) {
setupPropertyEnv(t)
flags, err := dnsProviderReportFlags("stdout", "")
if err != nil {
t.Fatalf("dnsProviderReportFlags: %v", err)
}
if len(flags) != 0 {
t.Errorf("flags = %v, want no flags", flagNames(flags))
}
}
func assertReportFuncValue(t *testing.T, flags map[string]common.ReportFunc, flagName string, want string) {
t.Helper()
fn, ok := flags[flagName]
if !ok {
t.Fatalf("flag %q missing from %v", flagName, flagNames(flags))
}
if got := fn("--global"); got != want {
t.Errorf("flag %q = %q, want %q", flagName, got, want)
}
}
func flagNames(flags map[string]common.ReportFunc) []string {
names := []string{}
for name := range flags {
names = append(names, name)
}
return names
}