From 691243e5fb151cdeee4aab8f91da82159afdc131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Feb 2021 21:06:36 +0100 Subject: [PATCH] Add annotation to use template path on disk We currently have two options to define vault agent templates: either define the template configuration as an inline template in the annotation or configure the vault agent directly. The former is really not handy when template is getting complex, the latter forces us to manage the whole vault agent configuration. We add a new annotation that enables the vault agent to inject secrets from a template file on the container disk. Since https://github.com/hashicorp/vault-k8s/pull/212, this template can be present in volume defined on the container. Annotation example: ```yaml vault.hashicorp.com/agent-inject-secret-foo: 'database/roles/app' vault.hashicorp.com/agent-inject-template-file-foo: '/etc/my-app/config.toml.tmpl' vault.hashicorp.com/agent-inject-file-foo: '/etc/my-app/config.toml', vault.hashicorp.com/agent-copy-volume-mounts: 'MyContainerNameWithVolumes' ``` If a template content is also defined in annotation (using `vault.hashicorp.com/agent-inject-template`, the template on disk won't be used. refs #84 --- agent-inject/agent/agent.go | 3 + agent-inject/agent/annotations.go | 15 +++ agent-inject/agent/annotations_test.go | 131 +++++++++++++++++++++++++ agent-inject/agent/config.go | 12 ++- agent-inject/agent/config_test.go | 15 ++- 5 files changed, 171 insertions(+), 5 deletions(-) diff --git a/agent-inject/agent/agent.go b/agent-inject/agent/agent.go index 75e94cb4..b0fd8c74 100644 --- a/agent-inject/agent/agent.go +++ b/agent-inject/agent/agent.go @@ -146,6 +146,9 @@ type Secret struct { // Template is the optional custom template to use when rendering the secret. Template string + // Template file is the optional path on disk to the custom template to use when rendering the secret. + TemplateFile string + // Mount Path for the volume holding the rendered secret file MountPath string diff --git a/agent-inject/agent/annotations.go b/agent-inject/agent/annotations.go index fc153c71..26c7762a 100644 --- a/agent-inject/agent/annotations.go +++ b/agent-inject/agent/annotations.go @@ -45,6 +45,15 @@ const ( // If not provided, a default generic template is used. AnnotationAgentInjectTemplate = "vault.hashicorp.com/agent-inject-template" + // AnnotationAgentInjectTemplateFile is the optional key annotation that configures Vault + // Agent what template on disk to use for rendering the secrets. The name + // of the template is any unique string after "vault.hashicorp.com/agent-inject-template-file-", + // such as "vault.hashicorp.com/agent-inject-template-file-foobar". This should map + // to the same unique value provided in "vault.hashicorp.com/agent-inject-secret-". + // The value is the filename and path of the template used by the agent to render the secrets. + // If not provided, the template content key annotation is used. + AnnotationAgentInjectTemplateFile = "vault.hashicorp.com/agent-inject-template-file" + // AnnotationAgentInjectToken is the annotation key for injecting the token // from auth/token/lookup-self AnnotationAgentInjectToken = "vault.hashicorp.com/agent-inject-token" @@ -379,6 +388,12 @@ func (a *Agent) secrets() []*Secret { if val, ok := a.Annotations[templateName]; ok { s.Template = val } + if s.Template == "" { + templateFileAnnotation := fmt.Sprintf("%s-%s", AnnotationAgentInjectTemplateFile, raw) + if val, ok := a.Annotations[templateFileAnnotation]; ok { + s.TemplateFile = val + } + } s.MountPath = a.Annotations[AnnotationVaultSecretVolumePath] mountPathAnnotationName := fmt.Sprintf("%s-%s", AnnotationVaultSecretVolumePath, raw) diff --git a/agent-inject/agent/annotations_test.go b/agent-inject/agent/annotations_test.go index 94857533..917e7701 100644 --- a/agent-inject/agent/annotations_test.go +++ b/agent-inject/agent/annotations_test.go @@ -473,6 +473,137 @@ func TestTemplateShortcuts(t *testing.T) { } } +func TestSecretMixedTemplatesAnnotations(t *testing.T) { + tests := []struct { + annotations map[string]string + expectedSecrets map[string]Secret + }{ + { + map[string]string{ + "vault.hashicorp.com/agent-inject-secret-foobar": "test1", + "vault.hashicorp.com/agent-inject-template-foobar": "", + "vault.hashicorp.com/agent-inject-template-file-foobar": "/etc/config.tmpl", + "vault.hashicorp.com/agent-inject-secret-test2": "test2", + "vault.hashicorp.com/agent-inject-template-test2": "foobarTemplate", + "vault.hashicorp.com/agent-inject-template-file-test2": "", + }, + map[string]Secret{ + "foobar": Secret{ + Name: "foobar", + Path: "test1", + Template: "", + TemplateFile: "/etc/config.tmpl", + MountPath: secretVolumePath, + }, + "test2": Secret{ + Name: "test2", + Path: "test2", + Template: "foobarTemplate", + TemplateFile: "", + MountPath: secretVolumePath, + }, + }, + }, + } + for _, tt := range tests { + pod := testPod(tt.annotations) + agentConfig := AgentConfig{ + "", "http://foobar:8200", "test", "test", true, "100", "1000", + DefaultAgentRunAsSameUser, DefaultAgentSetSecurityContext, + } + err := Init(pod, agentConfig) + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + var patches []*jsonpatch.JsonPatchOperation + + agent, err := New(pod, patches) + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + if len(agent.Secrets) != len(tt.expectedSecrets) { + t.Errorf("agent Secrets length was %d, expected %d", len(agent.Secrets), len(tt.expectedSecrets)) + } + + for _, s := range agent.Secrets { + if s == nil { + t.Error("Got a nil agent Secret") + t.FailNow() + } + expectedSecret, found := tt.expectedSecrets[s.Name] + if !found { + t.Errorf("Unexpected agent secret name %q", s.Name) + t.FailNow() + } + if !reflect.DeepEqual(expectedSecret, *s) { + t.Errorf("expected secret %+v, got agent secret %+v", expectedSecret, *s) + } + } + } +} + +func TestSecretTemplateFileAnnotations(t *testing.T) { + tests := []struct { + annotations map[string]string + expectedKey string + expectedTemplate string + expectedTemplateFile string + }{ + { + map[string]string{ + "vault.hashicorp.com/agent-inject-secret-foobar": "test1", + "vault.hashicorp.com/agent-inject-template-foobar": "foobarTemplate", + "vault.hashicorp.com/agent-inject-template-file-foobar": "/etc/config.tmpl", + }, "foobar", "foobarTemplate", "", + }, + { + map[string]string{ + "vault.hashicorp.com/agent-inject-secret-foobar": "test1", + "vault.hashicorp.com/agent-inject-template-foobar": "", + "vault.hashicorp.com/agent-inject-template-file-foobar": "/etc/config.tmpl", + }, "foobar", "", "/etc/config.tmpl", + }, + } + + for _, tt := range tests { + pod := testPod(tt.annotations) + var patches []*jsonpatch.JsonPatchOperation + + agentConfig := AgentConfig{ + "", "http://foobar:8200", "test", "test", true, "100", "1000", + DefaultAgentRunAsSameUser, DefaultAgentSetSecurityContext, + } + err := Init(pod, agentConfig) + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + agent, err := New(pod, patches) + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + if len(agent.Secrets) == 0 { + t.Error("Secrets length was zero, it shouldn't have been") + } + + if agent.Secrets[0].Name != tt.expectedKey { + t.Errorf("expected name %s, got %s", tt.expectedKey, agent.Secrets[0].Name) + } + + if agent.Secrets[0].Template != tt.expectedTemplate { + t.Errorf("expected template %s, got %s", tt.expectedTemplate, agent.Secrets[0].Template) + } + + if agent.Secrets[0].TemplateFile != tt.expectedTemplateFile { + t.Errorf("expected template file path %s, got %s", tt.expectedTemplateFile, agent.Secrets[0].TemplateFile) + } + + } +} + func TestSecretCommandAnnotations(t *testing.T) { tests := []struct { annotations map[string]string diff --git a/agent-inject/agent/config.go b/agent-inject/agent/config.go index c503954e..dc54e343 100644 --- a/agent-inject/agent/config.go +++ b/agent-inject/agent/config.go @@ -70,10 +70,11 @@ type Sink struct { type Template struct { CreateDestDirs bool `json:"create_dest_dirs,omitempty"` Destination string `json:"destination"` - Contents string `json:"contents"` + Contents string `json:"contents,omitempty"` LeftDelim string `json:"left_delimiter,omitempty"` RightDelim string `json:"right_delimiter,omitempty"` Command string `json:"command,omitempty"` + Source string `json:"source,omitempty"` } // Listener defines the configuration for Vault Agent Cache Listener @@ -92,8 +93,12 @@ func (a *Agent) newTemplateConfigs() []*Template { var templates []*Template for _, secret := range a.Secrets { template := secret.Template - if template == "" { - template = fmt.Sprintf(DefaultTemplate, secret.Path) + templateFile := secret.TemplateFile + if templateFile == "" { + template = secret.Template + if template == "" { + template = fmt.Sprintf(DefaultTemplate, secret.Path) + } } filePathAndName := fmt.Sprintf("%s/%s", secret.MountPath, secret.Name) @@ -102,6 +107,7 @@ func (a *Agent) newTemplateConfigs() []*Template { } tmpl := &Template{ + Source: templateFile, Contents: template, Destination: filePathAndName, LeftDelim: "{{", diff --git a/agent-inject/agent/config_test.go b/agent-inject/agent/config_test.go index 3a9a332f..4f51514b 100644 --- a/agent-inject/agent/config_test.go +++ b/agent-inject/agent/config_test.go @@ -32,6 +32,10 @@ func TestNewConfig(t *testing.T) { "vault.hashicorp.com/agent-inject-secret-different-path": "different-path", fmt.Sprintf("%s-%s", AnnotationVaultSecretVolumePath, "different-path"): "/etc/container_environment", + // render this secret from a template on disk + "vault.hashicorp.com/agent-inject-secret-with-file-template": "with-file-template", + fmt.Sprintf("%s-%s", AnnotationAgentInjectTemplateFile, "with-file-template"): "/etc/file-template", + "vault.hashicorp.com/agent-inject-command-bar": "pkill -HUP app", AnnotationAgentCacheEnable: "true", @@ -108,8 +112,8 @@ func TestNewConfig(t *testing.T) { t.Error("agent Cache should be disabled for init containers") } - if len(config.Templates) != 3 { - t.Errorf("expected 3 template, got %d", len(config.Templates)) + if len(config.Templates) != 4 { + t.Errorf("expected 4 template, got %d", len(config.Templates)) } for _, template := range config.Templates { @@ -136,6 +140,13 @@ func TestNewConfig(t *testing.T) { if template.Destination != "/etc/container_environment/different-path" { t.Errorf("expected template destination to be %s, got %s", "/etc/container_environment", template.Destination) } + } else if strings.Contains(template.Destination, "with-file-template") { + if template.Source != "/etc/file-template" { + t.Errorf("expected template file path to be %s, got %s", "/etc/file-template", template.Source) + } + if template.Contents != "" { + t.Errorf("expected template contents to be empty, got %s", template.Contents) + } } else { t.Error("shouldn't have got here") }