Skip to content

Commit

Permalink
Changed from env based to file based secret as source
Browse files Browse the repository at this point in the history
- this adds more flexibility secrets containing unknown/dynamic keys
- no need to specify (and know) every key inside a secret when configuring init
containers
  • Loading branch information
lynx-coding committed Oct 31, 2023
1 parent 05bfb15 commit 3b18292
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 49 deletions.
42 changes: 28 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@ We assume that applications configuration (files) are stored in [Configmaps](htt
All secrets needed by the application are also mounted to the init container, allowing the [templating](https://pkg.go.dev/text/template) mechanism to inject secrets into application configuration. The rendered templates are available to application containers using shared volumes between init containers and regular containers inside one Pod.

## Development
The example inside `examples/simple` can be used for development when operating locally.
The example inside `examples/simple` can be used for development when operating locally. Example docker command for local use is:
`docker run -v $(pwd)/tests/secrets:/etc/secrets -v $(pwd)/examples/simple/templates:/etc/templates -v $(pwd)/examples/rendered:/etc/rendered docker.io/library/k8s-multi-secret-to-file:local`
Make sure the directory mounted to `/etc/rendered` already exists.
### CLI parameters
| Parameter | Default value | Description |
|-------------------------|:--------------:|--------------------------------------------------------------------------------------------------------------------------------------|
| continue-on-missing-key | false | Templating of configfiles fails if a (secret) key is missing. This flag allows to continue on missing keys. |
| left-limiter | {{ | Left limiter for internal templating. Change if this limiter conflicts with your config format. |
| right-limiter | }} | Right limiter for internal templating. Change if this limiter conflicts with your config format. |
| secret-path | /etc/secrets | Path were secrets are read from. Can be changed for local development or testing. Should not matter when using the container. |
| target-base-dir | /etc/rendered | Path were rendered files are stored. Can be changed for local development or testing. Should not matter when using the container. |
| template-base-dir | /etc/templates | Path were template files are read from. Can be changed for local development or testing. Should not matter when using the container. |

## Setup
A working example can be found in `examples/k8s`. The files inside manifests directory can be deployed to a Kubernetes cluster.
Expand Down Expand Up @@ -46,21 +57,24 @@ A working example can be found in `examples/k8s`. The files inside manifests dir
...
```

4. provide secrets as environment variables to the init container. Envs must be prefixed (default: `SECRET_`)

4. provide secrets as files to the init container. `/etc/secrets` is the default secret path inside the init-container. For each Secret, configure the volumes:
```yaml
...
- env:
- name: SECRET_secret1
valueFrom:
secretKeyRef:
name: apache-demo
key: secret1
- name: SECRET_secret2
valueFrom:
secretKeyRef:
name: apache-demo
key: secret2
volumes:
- name: apache-demo
secret:
defaultMode: 420
secretName: apache-demo
...
```

and the volumeMounts:
```yaml
...
volumeMounts:
- mountPath: /etc/secrets/apache-demo
name: apache-demo
readOnly: true
...
```

Expand Down
2 changes: 2 additions & 0 deletions examples/rendered/etc/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test1=testvalue1
test2=testvalue2
4 changes: 2 additions & 2 deletions examples/simple/templates/etc/config
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
test1={{ .TEST1 }}
test2={{ .TEST2 }}
test1={{ .Secrets.example.TEST1 }}
test2={{ .Secrets.example.TEST2 }}
47 changes: 34 additions & 13 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
const (
LeftLimiter = "{{"
RightLimiter = "}}"
SecretPrefix = "SECRET_"
SecretPath = "/etc/secrets"
TargetBasePath = "/etc/rendered"
TemplateBasePath = "/etc/templates"
)
Expand All @@ -26,13 +26,16 @@ func main() {
continueOnMissingKey := flag.Bool("continue-on-missing-key", false, "enable to not stop when hitting missing keys during templating")
leftLimiter := flag.String("left-limiter", LeftLimiter, "left limiter for internal go templating")
rightLimiter := flag.String("right-limiter", RightLimiter, "right limiter for internal go templating")
secretEnvPrefix := flag.String("secret-env-prefix", SecretPrefix, "prefix for the environment variables containing secrets")
secretPath := flag.String("secret-path", SecretPath, "absolute path to directory where secrets are mounted")
targetBasePath := flag.String("target-base-dir", TargetBasePath, "absolute path to directory containing rendered template files")
templateBasePath := flag.String("template-base-dir", TemplateBasePath, "absolute path to directory containing template files")
flag.Parse()

// retrieve secrets
secrets := getSecretsFromEnv(*secretEnvPrefix)
secrets, err := getSecretsFromFiles(*secretPath)
if err != nil {
log.Panicf("failed to get secrets from files: %s", err)
}

// detect templates
templatePaths, err := getAllTemplateFilePaths(*templateBasePath)
Expand All @@ -47,7 +50,7 @@ func main() {
}
}

func parseTemplates(templatePaths []string, leftLimiter string, rightLimiter string, continueOnMissingKey bool, targetBasePath string, templateBasePath string, secrets map[string]string) error {
func parseTemplates(templatePaths []string, leftLimiter string, rightLimiter string, continueOnMissingKey bool, targetBasePath string, templateBasePath string, secrets map[string]map[string]string) error {
for _, templatePath := range templatePaths {
t, err := template.ParseFiles(templatePath)
if err != nil {
Expand All @@ -65,7 +68,11 @@ func parseTemplates(templatePaths []string, leftLimiter string, rightLimiter str
return fmt.Errorf("failed to create target dir for %q: %w", templatePath, err)
}
targetFile, _ := os.Create(targetPath)
err = t.Execute(targetFile, secrets)
err = t.Execute(targetFile, struct {
Secrets map[string]map[string]string
}{
Secrets: secrets,
})
if err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
Expand All @@ -87,17 +94,31 @@ func getAllTemplateFilePaths(templateWalkDir string) (templateFilePaths []string
return templateFilePaths, err
}

func getSecretsFromEnv(prefix string) map[string]string {
var secrets = make(map[string]string)
for _, envVar := range os.Environ() {
if strings.HasPrefix(envVar, prefix) {
parts := strings.SplitN(envVar, "=", 2)
if len(parts) == 2 {
secrets[strings.TrimPrefix(parts[0], prefix)] = parts[1]
func getSecretsFromFiles(secretsPath string) (map[string]map[string]string, error) {
secrets := make(map[string]map[string]string)
err := filepath.WalkDir(secretsPath, func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
secret, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read secret from file %q: %s", filePath, err)
}
keyName := path.Base(filePath)
secretName := path.Base(path.Dir(filePath))
_, ok := secrets[secretName]
if !ok {
secrets[secretName] = make(map[string]string)
}
secrets[secretName][keyName] = string(secret)
}
return nil
})
if err != nil {
return secrets, fmt.Errorf("failed to get secrets from files: %s", err)
}
return secrets
return secrets, err
}

func isDirectory(path string) bool {
Expand Down
54 changes: 34 additions & 20 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@ func Test_parseTemplates(t *testing.T) {
})
tests := []struct {
name string
secrets map[string]string
secrets map[string]map[string]string
tempPaths []string
continueOnMissingKey bool
wantError bool
expectedResult string
}{
{
name: "working example",
secrets: map[string]string{
"TEST1": "value1",
"TEST2": "value2",
secrets: map[string]map[string]string{
"example": {
"TEST1": "value1",
"TEST2": "value2",
},
},
tempPaths: []string{"examples/simple/templates/etc/config"},
continueOnMissingKey: false,
Expand All @@ -41,17 +43,21 @@ func Test_parseTemplates(t *testing.T) {
},
{
name: "missing secret",
secrets: map[string]string{
"TEST1": "value1",
secrets: map[string]map[string]string{
"example": {
"TEST1": "value1",
},
},
tempPaths: []string{"examples/simple/templates/etc/config"},
continueOnMissingKey: false,
wantError: true,
},
{
name: "missing secret with continue",
secrets: map[string]string{
"TEST1": "value1",
secrets: map[string]map[string]string{
"example": {
"TEST1": "value1",
},
},
tempPaths: []string{"examples/simple/templates/etc/config"},
continueOnMissingKey: true,
Expand All @@ -60,9 +66,11 @@ func Test_parseTemplates(t *testing.T) {
},
{
name: "wrong template path",
secrets: map[string]string{
"TEST1": "value1",
"TEST2": "value2",
secrets: map[string]map[string]string{
"example": {
"TEST1": "value1",
"TEST2": "value2",
},
},
tempPaths: []string{"examples/simple/templates/etc/config12345"},
continueOnMissingKey: false,
Expand All @@ -89,16 +97,22 @@ func Test_parseTemplates(t *testing.T) {
}
}

func Test_getSecretsFromEnv(t *testing.T) {
// prepare envs
_ = os.Setenv("TEST1", "value1")
_ = os.Setenv("SECRET_TEST1", "secretValue1")

wantedResult := map[string]string{
"TEST1": "secretValue1",
func Test_getSecretsFromFiles(t *testing.T) {
secrets, err := getSecretsFromFiles("tests/secrets")
if err != nil {
t.Errorf("failed to get secrets from files: %s", err)
}
if secrets["sec1"]["key1"] != "thisisavalue" {
t.Errorf("failed to map sec1[key1]: thisisavalue != %s", secrets["sec1"]["key1"])
}
if secrets["sec1"]["key2"] != "thisisanothervalue" {
t.Errorf("failed to map sec1[key2]: thisisanothervalue != %s", secrets["sec1"]["key2"])
}
if secrets["sec2"]["key1"] != "thisisjustavalue" {
t.Errorf("failed to map sec2[key1]: thisisjustavalue != %s", secrets["sec2"]["key1"])
}
if got := getSecretsFromEnv(SecretPrefix); !reflect.DeepEqual(got, wantedResult) {
t.Errorf("getSecretsFromEnv() = %v, want %v", got, wantedResult)
if secrets["sec2"]["key2"] != "thisisjustanothervalue" {
t.Errorf("failed to map sec2[key2]: thisisjustanothervalue != %s", secrets["sec2"]["key2"])
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/secrets/example/TEST1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testvalue1
1 change: 1 addition & 0 deletions tests/secrets/example/TEST2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testvalue2
1 change: 1 addition & 0 deletions tests/secrets/sec1/key1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thisisavalue
1 change: 1 addition & 0 deletions tests/secrets/sec1/key2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thisisanothervalue
1 change: 1 addition & 0 deletions tests/secrets/sec2/key1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thisisjustavalue
1 change: 1 addition & 0 deletions tests/secrets/sec2/key2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thisisjustanothervalue

0 comments on commit 3b18292

Please sign in to comment.