mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
cli: Add 'agent generate-config' sub-command (#20530)
This commit is contained in:
committed by
GitHub
parent
3d7d8f4965
commit
1a1af69cdd
402
command/agent_generate_config.go
Normal file
402
command/agent_generate_config.go
Normal file
@@ -0,0 +1,402 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
paths "path"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*AgentGenerateConfigCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*AgentGenerateConfigCommand)(nil)
|
||||
)
|
||||
|
||||
type AgentGenerateConfigCommand struct {
|
||||
*BaseCommand
|
||||
|
||||
flagType string
|
||||
flagPaths []string
|
||||
flagExec string
|
||||
}
|
||||
|
||||
func (c *AgentGenerateConfigCommand) Synopsis() string {
|
||||
return "Generate a Vault Agent configuration file."
|
||||
}
|
||||
|
||||
func (c *AgentGenerateConfigCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault agent generate-config [options] [args]
|
||||
|
||||
` + c.Flags().Help()
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AgentGenerateConfigCommand) Flags() *FlagSets {
|
||||
set := NewFlagSets(c.UI)
|
||||
|
||||
// Common Options
|
||||
f := set.NewFlagSet("Command Options")
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: "type",
|
||||
Target: &c.flagType,
|
||||
Usage: "Type of configuration file to generate; currently, only 'env-template' is supported.",
|
||||
Completion: complete.PredictSet(
|
||||
"env-template",
|
||||
),
|
||||
})
|
||||
|
||||
f.StringSliceVar(&StringSliceVar{
|
||||
Name: "path",
|
||||
Target: &c.flagPaths,
|
||||
Usage: "Path to a kv-v1 or kv-v2 secret (e.g. secret/data/foo, kv-v2/prefix/*); multiple secrets and tail '*' wildcards are allowed.",
|
||||
Completion: c.PredictVaultFolders(),
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: "exec",
|
||||
Target: &c.flagExec,
|
||||
Default: "env",
|
||||
Usage: "The command to execute in env-template mode.",
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *AgentGenerateConfigCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *AgentGenerateConfigCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *AgentGenerateConfigCommand) Run(args []string) int {
|
||||
flags := c.Flags()
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
args = flags.Args()
|
||||
|
||||
if len(args) > 1 {
|
||||
c.UI.Error(fmt.Sprintf("Too many arguments (expected at most 1, got %d)", len(args)))
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.flagType == "" {
|
||||
c.UI.Error(`Please specify a -type flag; currently only -type="env-template" is supported.`)
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.flagType != "env-template" {
|
||||
c.UI.Error(fmt.Sprintf(`%q is not a supported configuration type; currently only -type="env-template" is supported.`, c.flagType))
|
||||
return 1
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
config, err := generateConfiguration(context.Background(), client, c.flagExec, c.flagPaths)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error: %v", err))
|
||||
return 2
|
||||
}
|
||||
|
||||
var configPath string
|
||||
if len(args) == 1 {
|
||||
configPath = args[0]
|
||||
} else {
|
||||
configPath = "agent.hcl"
|
||||
}
|
||||
|
||||
f, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Could not create configuration file %q: %v", configPath, err))
|
||||
return 3
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Could not close configuration file %q: %v", configPath, err))
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := config.WriteTo(f); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Could not write to configuration file %q: %v", configPath, err))
|
||||
return 3
|
||||
}
|
||||
|
||||
c.UI.Info(fmt.Sprintf("Successfully generated %q configuration file!", configPath))
|
||||
|
||||
c.UI.Warn("Warning: the generated file uses 'token_file' authentication method, which is not suitable for production environments.")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func generateConfiguration(ctx context.Context, client *api.Client, flagExec string, flagPaths []string) (io.WriterTo, error) {
|
||||
var execCommand []string
|
||||
if flagExec != "" {
|
||||
execCommand = strings.Split(flagExec, " ")
|
||||
} else {
|
||||
execCommand = []string{"env"}
|
||||
}
|
||||
|
||||
tokenPath, err := homedir.Expand("~/.vault-token")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not expand home directory: %w", err)
|
||||
}
|
||||
|
||||
templates, err := constructTemplates(ctx, client, flagPaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate templates: %w", err)
|
||||
}
|
||||
|
||||
config := generatedConfig{
|
||||
AutoAuth: generatedConfigAutoAuth{
|
||||
Method: generatedConfigAutoAuthMethod{
|
||||
Type: "token_file",
|
||||
Config: generatedConfigAutoAuthMethodConfig{
|
||||
TokenFilePath: tokenPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
TemplateConfig: generatedConfigTemplateConfig{
|
||||
StaticSecretRenderInterval: "5m",
|
||||
ExitOnRetryFailure: true,
|
||||
},
|
||||
Vault: generatedConfigVault{
|
||||
Address: client.Address(),
|
||||
},
|
||||
Exec: generatedConfigExec{
|
||||
Command: execCommand,
|
||||
RestartOnSecretChanges: "always",
|
||||
RestartStopSignal: "SIGTERM",
|
||||
},
|
||||
EnvTemplates: templates,
|
||||
}
|
||||
|
||||
contents := hclwrite.NewEmptyFile()
|
||||
|
||||
gohcl.EncodeIntoBody(&config, contents.Body())
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func constructTemplates(ctx context.Context, client *api.Client, paths []string) ([]generatedConfigEnvTemplate, error) {
|
||||
var templates []generatedConfigEnvTemplate
|
||||
|
||||
for _, path := range paths {
|
||||
path = sanitizePath(path)
|
||||
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not validate secret path %q: %w", path, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(path, "/*"):
|
||||
// this path contains a tail wildcard, attempt to walk the tree
|
||||
t, err := constructTemplatesFromTree(ctx, client, path[:len(path)-2], mountPath, v2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not traverse sercet at %q: %w", path, err)
|
||||
}
|
||||
templates = append(templates, t...)
|
||||
|
||||
case strings.Contains(path, "*"):
|
||||
// don't allow any other wildcards
|
||||
return nil, fmt.Errorf("the path %q cannot contain '*' wildcard characters except as the last element of the path", path)
|
||||
|
||||
default:
|
||||
// regular secret path
|
||||
t, err := constructTemplatesFromSecret(ctx, client, path, mountPath, v2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read secret at %q: %v", path, err)
|
||||
}
|
||||
templates = append(templates, t...)
|
||||
}
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func constructTemplatesFromTree(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) {
|
||||
var templates []generatedConfigEnvTemplate
|
||||
|
||||
if v2 {
|
||||
metadataPath := strings.Replace(
|
||||
path,
|
||||
paths.Join(mountPath, "data"),
|
||||
paths.Join(mountPath, "metadata"),
|
||||
1,
|
||||
)
|
||||
if path != metadataPath {
|
||||
path = metadataPath
|
||||
} else {
|
||||
path = addPrefixToKVPath(path, mountPath, "metadata", true)
|
||||
}
|
||||
}
|
||||
|
||||
err := walkSecretsTree(ctx, client, path, func(child string, directory bool) error {
|
||||
if directory {
|
||||
return nil
|
||||
}
|
||||
|
||||
dataPath := strings.Replace(
|
||||
child,
|
||||
paths.Join(mountPath, "metadata"),
|
||||
paths.Join(mountPath, "data"),
|
||||
1,
|
||||
)
|
||||
|
||||
t, err := constructTemplatesFromSecret(ctx, client, dataPath, mountPath, v2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
templates = append(templates, t...)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func constructTemplatesFromSecret(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) {
|
||||
var templates []generatedConfigEnvTemplate
|
||||
|
||||
if v2 {
|
||||
path = addPrefixToKVPath(path, mountPath, "data", true)
|
||||
}
|
||||
|
||||
resp, err := client.Logical().ReadWithContext(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error querying: %w", err)
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("secret not found")
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if v2 {
|
||||
internal, ok := resp.Data["data"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secret.Data not found")
|
||||
}
|
||||
data = internal.(map[string]interface{})
|
||||
} else {
|
||||
data = resp.Data
|
||||
}
|
||||
|
||||
fields := make([]string, 0, len(data))
|
||||
|
||||
for field := range data {
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
// sort for a deterministic output
|
||||
sort.Strings(fields)
|
||||
|
||||
var dataContents string
|
||||
if v2 {
|
||||
dataContents = ".Data.data"
|
||||
} else {
|
||||
dataContents = ".Data"
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
templates = append(templates, generatedConfigEnvTemplate{
|
||||
Name: constructDefaultEnvironmentKey(path, field),
|
||||
Contents: fmt.Sprintf(`{{ with secret "%s" }}{{ %s.%s }}{{ end }}`, path, dataContents, field),
|
||||
ErrorOnMissingKey: true,
|
||||
})
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
func constructDefaultEnvironmentKey(path string, field string) string {
|
||||
pathParts := strings.Split(path, "/")
|
||||
pathPartsLast := pathParts[len(pathParts)-1]
|
||||
|
||||
notLetterOrNumber := func(r rune) bool {
|
||||
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
|
||||
}
|
||||
|
||||
p1 := strings.FieldsFunc(pathPartsLast, notLetterOrNumber)
|
||||
p2 := strings.FieldsFunc(field, notLetterOrNumber)
|
||||
|
||||
keyParts := append(p1, p2...)
|
||||
|
||||
return strings.ToUpper(strings.Join(keyParts, "_"))
|
||||
}
|
||||
|
||||
// Below, we are redefining a subset of the configuration-related structures
|
||||
// defined under command/agent/config. Using these structures we can tailor the
|
||||
// output of the generated config, while using the original structures would
|
||||
// have produced an HCL document with many empty fields. The structures below
|
||||
// should not be used for anything other than generation.
|
||||
|
||||
type generatedConfig struct {
|
||||
AutoAuth generatedConfigAutoAuth `hcl:"auto_auth,block"`
|
||||
TemplateConfig generatedConfigTemplateConfig `hcl:"template_config,block"`
|
||||
Vault generatedConfigVault `hcl:"vault,block"`
|
||||
EnvTemplates []generatedConfigEnvTemplate `hcl:"env_template,block"`
|
||||
Exec generatedConfigExec `hcl:"exec,block"`
|
||||
}
|
||||
|
||||
type generatedConfigTemplateConfig struct {
|
||||
StaticSecretRenderInterval string `hcl:"static_secret_render_interval"`
|
||||
ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"`
|
||||
}
|
||||
|
||||
type generatedConfigExec struct {
|
||||
Command []string `hcl:"command"`
|
||||
RestartOnSecretChanges string `hcl:"restart_on_secret_changes"`
|
||||
RestartStopSignal string `hcl:"restart_stop_signal"`
|
||||
}
|
||||
|
||||
type generatedConfigEnvTemplate struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Contents string `hcl:"contents,attr"`
|
||||
ErrorOnMissingKey bool `hcl:"error_on_missing_key"`
|
||||
}
|
||||
|
||||
type generatedConfigVault struct {
|
||||
Address string `hcl:"address"`
|
||||
}
|
||||
|
||||
type generatedConfigAutoAuth struct {
|
||||
Method generatedConfigAutoAuthMethod `hcl:"method,block"`
|
||||
}
|
||||
|
||||
type generatedConfigAutoAuthMethod struct {
|
||||
Type string `hcl:"type"`
|
||||
Config generatedConfigAutoAuthMethodConfig `hcl:"config,block"`
|
||||
}
|
||||
|
||||
type generatedConfigAutoAuthMethodConfig struct {
|
||||
TokenFilePath string `hcl:"token_file_path"`
|
||||
}
|
||||
274
command/agent_generate_config_test.go
Normal file
274
command/agent_generate_config_test.go
Normal file
@@ -0,0 +1,274 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestConstructTemplates tests the construcTemplates helper function
|
||||
func TestConstructTemplates(t *testing.T) {
|
||||
ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelContextFunc()
|
||||
|
||||
client, closer := testVaultServerWithSecrets(ctx, t)
|
||||
defer closer()
|
||||
|
||||
cases := map[string]struct {
|
||||
paths []string
|
||||
expected []generatedConfigEnvTemplate
|
||||
expectedError bool
|
||||
}{
|
||||
"kv-v1-simple": {
|
||||
paths: []string{"kv-v1/foo"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v2-simple": {
|
||||
paths: []string{"kv-v2/foo"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v2-data-in-path": {
|
||||
paths: []string{"kv-v2/data/foo"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v1-nested": {
|
||||
paths: []string{"kv-v1/app-1/*"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/foo" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/foo" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/nested/baz" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/nested/baz" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v2-nested": {
|
||||
paths: []string{"kv-v2/app-1/*"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/nested/baz" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/nested/baz" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAZ_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v1-multi-path": {
|
||||
paths: []string{"kv-v1/foo", "kv-v1/app-1/bar"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v1/foo" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v1/app-1/bar" }}{{ .Data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v2-multi-path": {
|
||||
paths: []string{"kv-v2/foo", "kv-v2/app-1/bar"},
|
||||
expected: []generatedConfigEnvTemplate{
|
||||
{Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/foo" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "FOO_USER"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.password }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_PASSWORD"},
|
||||
{Contents: `{{ with secret "kv-v2/data/app-1/bar" }}{{ .Data.data.user }}{{ end }}`, ErrorOnMissingKey: true, Name: "BAR_USER"},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v1-path-not-found": {
|
||||
paths: []string{"kv-v1/does/not/exist"},
|
||||
expected: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
|
||||
"kv-v2-path-not-found": {
|
||||
paths: []string{"kv-v2/does/not/exist"},
|
||||
expected: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
|
||||
"kv-v1-early-wildcard": {
|
||||
paths: []string{"kv-v1/*/foo"},
|
||||
expected: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
|
||||
"kv-v2-early-wildcard": {
|
||||
paths: []string{"kv-v2/*/foo"},
|
||||
expected: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
templates, err := constructTemplates(ctx, client, tc.paths)
|
||||
|
||||
if tc.expectedError {
|
||||
if err == nil {
|
||||
t.Fatal("an error was expected but the test succeeded")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.expected, templates) {
|
||||
t.Fatalf("unexpected output; want: %v, got: %v", tc.expected, templates)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateConfiguration tests the generateConfiguration helper function
|
||||
func TestGenerateConfiguration(t *testing.T) {
|
||||
ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelContextFunc()
|
||||
|
||||
client, closer := testVaultServerWithSecrets(ctx, t)
|
||||
defer closer()
|
||||
|
||||
cases := map[string]struct {
|
||||
flagExec string
|
||||
flagPaths []string
|
||||
expected *regexp.Regexp
|
||||
expectedError bool
|
||||
}{
|
||||
"kv-v1-simple": {
|
||||
flagExec: "./my-app arg1 arg2",
|
||||
flagPaths: []string{"kv-v1/foo"},
|
||||
expected: regexp.MustCompile(`
|
||||
auto_auth \{
|
||||
|
||||
method \{
|
||||
type = "token_file"
|
||||
|
||||
config \{
|
||||
token_file_path = ".*/.vault-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template_config \{
|
||||
static_secret_render_interval = "5m"
|
||||
exit_on_retry_failure = true
|
||||
}
|
||||
|
||||
vault \{
|
||||
address = "https://127.0.0.1:[0-9]{5}"
|
||||
}
|
||||
|
||||
env_template "FOO_PASSWORD" \{
|
||||
contents = "\{\{ with secret \\"kv-v1/foo\\" }}\{\{ .Data.password }}\{\{ end }}"
|
||||
error_on_missing_key = true
|
||||
}
|
||||
env_template "FOO_USER" \{
|
||||
contents = "\{\{ with secret \\"kv-v1/foo\\" }}\{\{ .Data.user }}\{\{ end }}"
|
||||
error_on_missing_key = true
|
||||
}
|
||||
|
||||
exec \{
|
||||
command = \["./my-app", "arg1", "arg2"\]
|
||||
restart_on_secret_changes = "always"
|
||||
restart_stop_signal = "SIGTERM"
|
||||
}
|
||||
`),
|
||||
expectedError: false,
|
||||
},
|
||||
|
||||
"kv-v2-default-exec": {
|
||||
flagExec: "",
|
||||
flagPaths: []string{"kv-v2/foo"},
|
||||
expected: regexp.MustCompile(`
|
||||
auto_auth \{
|
||||
|
||||
method \{
|
||||
type = "token_file"
|
||||
|
||||
config \{
|
||||
token_file_path = ".*/.vault-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template_config \{
|
||||
static_secret_render_interval = "5m"
|
||||
exit_on_retry_failure = true
|
||||
}
|
||||
|
||||
vault \{
|
||||
address = "https://127.0.0.1:[0-9]{5}"
|
||||
}
|
||||
|
||||
env_template "FOO_PASSWORD" \{
|
||||
contents = "\{\{ with secret \\"kv-v2/data/foo\\" }}\{\{ .Data.data.password }}\{\{ end }}"
|
||||
error_on_missing_key = true
|
||||
}
|
||||
env_template "FOO_USER" \{
|
||||
contents = "\{\{ with secret \\"kv-v2/data/foo\\" }}\{\{ .Data.data.user }}\{\{ end }}"
|
||||
error_on_missing_key = true
|
||||
}
|
||||
|
||||
exec \{
|
||||
command = \["env"\]
|
||||
restart_on_secret_changes = "always"
|
||||
restart_stop_signal = "SIGTERM"
|
||||
}
|
||||
`),
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
name, tc := name, tc
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var config bytes.Buffer
|
||||
|
||||
c, err := generateConfiguration(ctx, client, tc.flagExec, tc.flagPaths)
|
||||
c.WriteTo(&config)
|
||||
|
||||
if tc.expectedError {
|
||||
if err == nil {
|
||||
t.Fatal("an error was expected but the test succeeded")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !tc.expected.MatchString(config.String()) {
|
||||
t.Fatalf("unexpected output; want: %v, got: %v", tc.expected.String(), config.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,50 @@ func testVaultServer(tb testing.TB) (*api.Client, func()) {
|
||||
return client, closer
|
||||
}
|
||||
|
||||
func testVaultServerWithSecrets(ctx context.Context, tb testing.TB) (*api.Client, func()) {
|
||||
tb.Helper()
|
||||
|
||||
client, _, closer := testVaultServerUnseal(tb)
|
||||
|
||||
// enable kv-v1 backend
|
||||
if err := client.Sys().Mount("kv-v1/", &api.MountInput{
|
||||
Type: "kv-v1",
|
||||
}); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
// enable kv-v2 backend
|
||||
if err := client.Sys().Mount("kv-v2/", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
}); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
// populate dummy secrets
|
||||
for _, path := range []string{
|
||||
"foo",
|
||||
"app-1/foo",
|
||||
"app-1/bar",
|
||||
"app-1/nested/baz",
|
||||
} {
|
||||
if err := client.KVv1("kv-v1").Put(ctx, path, map[string]interface{}{
|
||||
"user": "test",
|
||||
"password": "Hashi123",
|
||||
}); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{
|
||||
"user": "test",
|
||||
"password": "Hashi123",
|
||||
}); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return client, closer
|
||||
}
|
||||
|
||||
func testVaultServerWithKVVersion(tb testing.TB, kvVersion string) (*api.Client, func()) {
|
||||
tb.Helper()
|
||||
|
||||
|
||||
@@ -268,6 +268,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co
|
||||
SighupCh: MakeSighupCh(),
|
||||
}, nil
|
||||
},
|
||||
"agent generate-config": func() (cli.Command, error) {
|
||||
return &AgentGenerateConfigCommand{
|
||||
BaseCommand: getBaseCommand(),
|
||||
}, nil
|
||||
},
|
||||
"audit": func() (cli.Command, error) {
|
||||
return &AuditCommand{
|
||||
BaseCommand: getBaseCommand(),
|
||||
|
||||
Reference in New Issue
Block a user