From 00838ba403e40f5832be473ecfcc04022abb4da9 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 19 Feb 2024 18:25:00 +0100 Subject: [PATCH] Values without a backing env var should not be expanded --- cmd/backup/config_provider.go | 38 +++++++++++-- cmd/backup/expand.go | 100 ++++++++++++++++++++++++++++++++++ test/confd/02backup.env | 1 + test/confd/run.sh | 2 +- 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 cmd/backup/expand.go diff --git a/cmd/backup/config_provider.go b/cmd/backup/config_provider.go index 4225d708..8414305f 100644 --- a/cmd/backup/config_provider.go +++ b/cmd/backup/config_provider.go @@ -4,6 +4,7 @@ package main import ( + "bufio" "fmt" "os" "path/filepath" @@ -99,11 +100,7 @@ func loadConfigsFromEnvFiles(directory string) ([]*Config, error) { continue } p := filepath.Join(directory, item.Name()) - f, err := os.ReadFile(p) - if err != nil { - return nil, errwrap.Wrap(err, fmt.Sprintf("error reading %s", item.Name())) - } - envFile, err := godotenv.Unmarshal(os.ExpandEnv(string(f))) + envFile, err := source(p) if err != nil { return nil, errwrap.Wrap(err, fmt.Sprintf("error reading config file %s", p)) } @@ -125,3 +122,34 @@ func loadConfigsFromEnvFiles(directory string) ([]*Config, error) { return configs, nil } + +func source(path string) (map[string]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, errwrap.Wrap(err, fmt.Sprintf("error opening %s", path)) + } + + result := map[string]string{} + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + line = expand(line, os.LookupEnv) + m, err := godotenv.Unmarshal(line) + if err != nil { + return nil, errwrap.Wrap(err, fmt.Sprintf("error sourcing %s", path)) + } + for key, value := range m { + currentValue, currentOk := os.LookupEnv(key) + defer func() { + if currentOk { + os.Setenv(key, currentValue) + return + } + os.Unsetenv(key) + }() + result[key] = value + os.Setenv(key, value) + } + } + return result, nil +} diff --git a/cmd/backup/expand.go b/cmd/backup/expand.go new file mode 100644 index 00000000..d143f839 --- /dev/null +++ b/cmd/backup/expand.go @@ -0,0 +1,100 @@ +// This code comes from golang standard library: +// https://github.com/golang/go/blob/0e85fd7561de869add933801c531bf25dee9561c/src/os/env.go#L16-L96 +// This file is explicitly excluded from the top repository license. + +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// General environment variables. + +package main + +import "fmt" + +// Expand replaces ${var} or $var in the string based on the mapping function. +func expand(s string, mapping func(string) (string, bool)) string { + var buf []byte + // ${} is all ASCII, so bytes are fine for this operation. + i := 0 + for j := 0; j < len(s); j++ { + if s[j] == '$' && j+1 < len(s) { + if buf == nil { + buf = make([]byte, 0, 2*len(s)) + } + buf = append(buf, s[i:j]...) + shellNameInput := s[j+1:] + name, w := getShellName(shellNameInput) + if name == "" && w > 0 { + // Encountered invalid syntax; eat the + // characters. + } else if name == "" { + // Valid syntax, but $ was not followed by a + // name. Leave the dollar character untouched. + buf = append(buf, s[j]) + } else { + replacement, ok := mapping(name) + if ok { + buf = append(buf, replacement...) + } else { + // preserve enclosing {} + if shellNameInput[0] == '{' { + buf = append(buf, fmt.Sprintf("${%s}", name)...) + } else { + buf = append(buf, fmt.Sprintf("$%s", name)...) + } + } + } + j += w + i = j + 1 + } + } + if buf == nil { + return s + } + return string(buf) + s[i:] +} + +// isShellSpecialVar reports whether the character identifies a special +// shell variable such as $*. +func isShellSpecialVar(c uint8) bool { + switch c { + case '*', '#', '$', '@', '!', '?', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true + } + return false +} + +// isAlphaNum reports whether the byte is an ASCII letter, number, or underscore +func isAlphaNum(c uint8) bool { + return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' +} + +// getShellName returns the name that begins the string and the number of bytes +// consumed to extract it. If the name is enclosed in {}, it's part of a ${} +// expansion and two more bytes are needed than the length of the name. +func getShellName(s string) (string, int) { + switch { + case s[0] == '{': + if len(s) > 2 && isShellSpecialVar(s[1]) && s[2] == '}' { + return s[1:2], 3 + } + // Scan to closing brace + for i := 1; i < len(s); i++ { + if s[i] == '}' { + if i == 1 { + return "", 2 // Bad syntax; eat "${}" + } + return s[1:i], i + 1 + } + } + return "", 1 // Bad syntax; eat "${" + case isShellSpecialVar(s[0]): + return s[0:1], 1 + } + // Scan alphanumerics. + var i int + for i = 0; i < len(s) && isAlphaNum(s[i]); i++ { + } + return s[:i], i +} diff --git a/test/confd/02backup.env b/test/confd/02backup.env index 4278acb4..09bab41a 100644 --- a/test/confd/02backup.env +++ b/test/confd/02backup.env @@ -1,2 +1,3 @@ NAME="other" BACKUP_CRON_EXPRESSION="*/1 * * * *" +BACKUP_FILENAME="override-$NAME.tar.gz" diff --git a/test/confd/run.sh b/test/confd/run.sh index f81407a5..b722542e 100755 --- a/test/confd/run.sh +++ b/test/confd/run.sh @@ -20,7 +20,7 @@ if [ ! -f "$LOCAL_DIR/conf.tar.gz" ]; then fi pass "Config from file was used." -if [ ! -f "$LOCAL_DIR/other.tar.gz" ]; then +if [ ! -f "$LOCAL_DIR/override-other.tar.gz" ]; then fail "Run on same schedule did not succeed." fi pass "Run on same schedule succeeded."