mirror of
https://github.com/dokku/dokku.git
synced 2026-08-29 10:08:53 +02:00
fix: report traefik dns-provider env vars as global keys
The dynamic `dns-provider-*` properties were reported as `--traefik-dns-provider-<env_var>`, outside the `global`/`computed` namespace every other traefik property uses, so a consumer reading the json report could not tell which of the two a key represented. They are now reported as `--traefik-global-dns-provider-<env_var>`. The `basic-auth-password` property is a credential as well and is now masked in the default report output on the same terms as the dns provider values, meaning the raw value is returned only for `--format json` or when the flag is queried by name.
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
143
plugins/traefik-vhosts/report_test.go
Normal file
143
plugins/traefik-vhosts/report_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user