From 0b035ddfe104c72a8aec458ad67e3336a39f44b2 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Thu, 4 May 2023 14:38:32 -0400 Subject: [PATCH 01/21] tmp --- command/agent_generate_config.go | 309 +++++++++++++++++++++++++++++++ go.mod | 4 + go.sum | 9 + 3 files changed, 322 insertions(+) create mode 100644 command/agent_generate_config.go diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go new file mode 100644 index 000000000000..42274cba3893 --- /dev/null +++ b/command/agent_generate_config.go @@ -0,0 +1,309 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "context" + "fmt" + "os" + paths "path" + "regexp" + "sort" + "strings" + "time" + + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/command/agent/config" + "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 + flagSecrets []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, + Default: "env-template", + Usage: "Type of configuration file to generate; currently, only 'env-template' is supported.", + Completion: complete.PredictSet( + "env-template", + ), + }) + + f.StringSliceVar(&StringSliceVar{ + Name: "secret", + Target: &c.flagSecrets, + 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 for 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 { + ctx := context.Background() + + 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 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + var templates []*config.EnvTemplateGen + + for _, path := range c.flagSecrets { + pathSanitized := sanitizePath(path) + pathMount, v2, err := isKVv2(pathSanitized, client) + if err != nil { + c.UI.Error(fmt.Sprintf("Could not validate secret path %q: %v", path, err)) + return 2 + } + + if strings.HasSuffix(pathSanitized, "/*") { + t, err := traverseSecrets(ctx, client, pathSanitized[:len(pathSanitized)-2], pathMount, v2) + if err != nil { + c.UI.Error(fmt.Sprintf("Could not traverse secret at %q: %v", pathSanitized[:len(pathSanitized)-2], err)) + return 2 + } + templates = append(templates, t...) + } else { + t, err := readSecret(ctx, client, pathSanitized, pathMount, v2) + if err != nil { + c.UI.Error(fmt.Sprintf("Could not read secret at %q: %v", pathSanitized, err)) + return 2 + } + templates = append(templates, t...) + } + } + + var execCommand string + if c.flagExec != "" { + execCommand = c.flagExec + } else { + execCommand = "env" + } + + tokenPath, err := homedir.Expand("~/.vault-token") + if err != nil { + c.UI.Error(fmt.Sprintf("Could not expand home directory: %v", err)) + return 2 + } + + agentConfig := config.ConfigGen{ + Vault: &config.VaultGen{ + Address: client.Address(), + }, + AutoAuth: &config.AutoAuthGen{ + Method: &config.AutoAuthMethodGen{ + Type: "token_file", + Config: config.AutoAuthMethodConfigGen{ + TokenFilePath: tokenPath, + }, + }, + }, + EnvTemplates: templates, + Exec: &config.ExecConfig{ + Command: execCommand, + Args: []string{}, + RestartOnNewSecret: "always", + RestartKillSignal: "SIGTERM", + }, + } + + var configPath string + if len(args) == 1 { + configPath = args[0] + } else { + configPath = "agent.hcl" + } + + contents := hclwrite.NewEmptyFile() + + gohcl.EncodeIntoBody(&agentConfig, contents.Body()) + + f, err := os.Create(configPath) + if err != nil { + c.UI.Error(fmt.Sprintf("Could not create configuration file %q: %v", configPath, err)) + return 1 + } + defer func() { + if err := f.Close(); err != nil { + c.UI.Error(fmt.Sprintf("Could not close configuration file %q: %v", configPath, err)) + } + }() + + if _, err := contents.WriteTo(f); err != nil { + c.UI.Error(fmt.Sprintf("Could not write to configuration file %q: %v", configPath, err)) + return 1 + } + + c.UI.Info(fmt.Sprintf("Successfully generated %q configuration file!", configPath)) + + return 0 +} + +func traverseSecrets(ctx context.Context, client *api.Client, path, pathMount string, v2 bool) ([]*config.EnvTemplateGen, error) { + var templates []*config.EnvTemplateGen + + if v2 { + path = addPrefixToKVPath(path, pathMount, "metadata", true) + } + + resp, err := client.Logical().ListWithContext(ctx, path) + if err != nil { + return nil, fmt.Errorf("error querying: %w", err) + } + + if resp != nil { + k, ok := resp.Data["keys"] + if !ok { + return nil, fmt.Errorf("unexpected list response: %v", resp.Data) + } + + keys, ok := k.([]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected list response type %T", k) + } + + for _, key := range keys { + t, err := traverseSecrets(ctx, client, paths.Join(path, key.(string)), pathMount, v2) + if err != nil { + return nil, err + } + templates = append(templates, t...) + } + } else { + t, err := readSecret(ctx, client, path, pathMount, v2) + if err != nil { + return nil, err + } + templates = append(templates, t...) + } + + return templates, nil +} + +func readSecret(ctx context.Context, client *api.Client, path, pathMount string, v2 bool) ([]*config.EnvTemplateGen, error) { + var templates []*config.EnvTemplateGen + + if v2 { + path = addPrefixToKVPath(path, pathMount, "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") + } + + data := resp.Data + if v2 { + internal, ok := resp.Data["data"] + if !ok { + return nil, fmt.Errorf("secret.Data not found") + } + data = internal.(map[string]interface{}) + } + + var fields []string + + for field := range data { + fields = append(fields, field) + } + + sort.Strings(fields) + + for _, field := range fields { + v2AdjustedField := field + if v2 { + v2AdjustedField = "data." + field + } + templates = append(templates, &config.EnvTemplateGen{ + Name: constructDefaultEnvironmentKey(path, field), + Contents: fmt.Sprintf(`{{ with secret "%s" }}{{ .Data.%s }}{{ end }}`, path, v2AdjustedField), + ErrorOnMissingKey: true, + }) + } + + return templates, nil +} + +func constructDefaultEnvironmentKey(path string, field string) string { + pathParts := strings.Split(path, "/") + pathPartsLast := pathParts[len(pathParts)-1] + + nonWordRegex := regexp.MustCompile(`[^\w]+`) // match a sequence of non-word characters + + p1 := nonWordRegex.Split(pathPartsLast, -1) + p2 := nonWordRegex.Split(field, -1) + + keyParts := append(p1, p2...) + + return strings.ToUpper(strings.Join(keyParts, "_")) + +} diff --git a/go.mod b/go.mod index fa2ce46e6700..f028c62ed7c3 100644 --- a/go.mod +++ b/go.mod @@ -108,6 +108,7 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/hcl v1.0.1-vault-5 + github.com/hashicorp/hcl/v2 v2.16.2 github.com/hashicorp/hcp-link v0.1.0 github.com/hashicorp/hcp-scada-provider v0.2.1 github.com/hashicorp/hcp-sdk-go v0.23.0 @@ -258,8 +259,10 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/agext/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.3.2 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0 // indirect @@ -451,6 +454,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + github.com/zclconf/go-cty v1.12.1 // indirect go.etcd.io/etcd/api/v3 v3.5.7 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.7.0 // indirect diff --git a/go.sum b/go.sum index 90c25af40815..da81b58e565b 100644 --- a/go.sum +++ b/go.sum @@ -689,6 +689,8 @@ github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KM github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/aerospike/aerospike-client-go/v5 v5.6.0 h1:tRxcUq0HY8fFPQEzF3EgrknF+w1xFO0YDfUb9Nm8yRI= github.com/aerospike/aerospike-client-go/v5 v5.6.0/go.mod h1:rJ/KpmClE7kiBPfvAPrGw9WuNOiz8v2uKbQaUyYPXtI= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= @@ -717,6 +719,8 @@ github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64 h1:ZsPrlYPY/ github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2 h1:VoHKYIXEQU5LWoambPBOvYxyLqZYHuj+rj5DVnMUc3k= github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2/go.mod h1:OMVSB21p9+xQUIqlGizHPZfjK+SHws1ht+ZytVDoz9U= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -1750,6 +1754,8 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcl/v2 v2.16.2 h1:mpkHZh/Tv+xet3sy3F9Ld4FyI2tUpWe9x3XtPx9f1a0= +github.com/hashicorp/hcl/v2 v2.16.2/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng= github.com/hashicorp/hcp-link v0.1.0 h1:F6F1cpADc+o5EBI5CbJn5RX4qdFSLpuA4fN69eeE5lQ= github.com/hashicorp/hcp-link v0.1.0/go.mod h1:BWVDuJDHrKJtWc5qI07bX5xlLjSgWq6kYLQUeG1g5dM= github.com/hashicorp/hcp-scada-provider v0.2.1 h1:yr+Uxini7SWTZ2t49d3Xi+6+X/rbsSFx8gq6WVcC91c= @@ -2488,6 +2494,7 @@ github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvW github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sethvargo/go-limiter v0.7.1 h1:wWNhTj0pxjyJ7wuJHpRJpYwJn+bUnjYfw2a85eu5w9U= github.com/sethvargo/go-limiter v0.7.1/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU= @@ -2680,6 +2687,8 @@ github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY= +github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= From 2a8088607edf5868b256c683537c74f9e205956e Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Thu, 4 May 2023 21:19:58 -0400 Subject: [PATCH 02/21] refactor stuff --- command/agent_generate_config.go | 201 +++++++++++++++++++------------ command/commands.go | 5 + 2 files changed, 128 insertions(+), 78 deletions(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 42274cba3893..8d77d5a13bab 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -7,16 +7,13 @@ import ( "context" "fmt" "os" - paths "path" "regexp" "sort" "strings" - "time" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/config" "github.com/mitchellh/cli" "github.com/mitchellh/go-homedir" "github.com/posener/complete" @@ -75,7 +72,7 @@ func (c *AgentGenerateConfigCommand) Flags() *FlagSets { Name: "exec", Target: &c.flagExec, Default: "env", - Usage: "The command to execute for in env-template mode.", + Usage: "The command to execute in env-template mode.", }) return set @@ -112,38 +109,17 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { return 2 } - var templates []*config.EnvTemplateGen - - for _, path := range c.flagSecrets { - pathSanitized := sanitizePath(path) - pathMount, v2, err := isKVv2(pathSanitized, client) - if err != nil { - c.UI.Error(fmt.Sprintf("Could not validate secret path %q: %v", path, err)) - return 2 - } - - if strings.HasSuffix(pathSanitized, "/*") { - t, err := traverseSecrets(ctx, client, pathSanitized[:len(pathSanitized)-2], pathMount, v2) - if err != nil { - c.UI.Error(fmt.Sprintf("Could not traverse secret at %q: %v", pathSanitized[:len(pathSanitized)-2], err)) - return 2 - } - templates = append(templates, t...) - } else { - t, err := readSecret(ctx, client, pathSanitized, pathMount, v2) - if err != nil { - c.UI.Error(fmt.Sprintf("Could not read secret at %q: %v", pathSanitized, err)) - return 2 - } - templates = append(templates, t...) - } + templates, err := fetchTemplates(ctx, client, c.flagSecrets) + if err != nil { + c.UI.Error(fmt.Sprintf("Error generating templates: %v", err)) + return 2 } - var execCommand string + var execCommand []string if c.flagExec != "" { - execCommand = c.flagExec + execCommand = strings.Split(c.flagExec, " ") } else { - execCommand = "env" + execCommand = []string{"env"} } tokenPath, err := homedir.Expand("~/.vault-token") @@ -152,24 +128,23 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { return 2 } - agentConfig := config.ConfigGen{ - Vault: &config.VaultGen{ + config := generatedConfig{ + Vault: &generatedConfigVault{ Address: client.Address(), }, - AutoAuth: &config.AutoAuthGen{ - Method: &config.AutoAuthMethodGen{ + AutoAuth: &generatedConfigAutoAuth{ + Method: &generatedConfigAutoAuthMethod{ Type: "token_file", - Config: config.AutoAuthMethodConfigGen{ + Config: generatedConfigAutoAuthMethodConfig{ TokenFilePath: tokenPath, }, }, }, EnvTemplates: templates, - Exec: &config.ExecConfig{ - Command: execCommand, - Args: []string{}, - RestartOnNewSecret: "always", - RestartKillSignal: "SIGTERM", + Exec: &generatedConfigExec{ + Command: execCommand, + RestartOnSecretChanges: true, + RestartKillSignal: "SIGTERM", }, } @@ -182,7 +157,7 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { contents := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(&agentConfig, contents.Body()) + gohcl.EncodeIntoBody(&config, contents.Body()) f, err := os.Create(configPath) if err != nil { @@ -205,52 +180,76 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { return 0 } -func traverseSecrets(ctx context.Context, client *api.Client, path, pathMount string, v2 bool) ([]*config.EnvTemplateGen, error) { - var templates []*config.EnvTemplateGen +func fetchTemplates(ctx context.Context, client *api.Client, secretPaths []string) ([]generatedConfigEnvTemplate, error) { + var templates []generatedConfigEnvTemplate - if v2 { - path = addPrefixToKVPath(path, pathMount, "metadata", true) - } + for _, path := range secretPaths { + path = sanitizePath(path) - resp, err := client.Logical().ListWithContext(ctx, path) - if err != nil { - return nil, fmt.Errorf("error querying: %w", err) - } - - if resp != nil { - k, ok := resp.Data["keys"] - if !ok { - return nil, fmt.Errorf("unexpected list response: %v", resp.Data) + mountPath, v2, err := isKVv2(path, client) + if err != nil { + return nil, fmt.Errorf("could not validate secret path %q: %w", path, err) } - keys, ok := k.([]interface{}) - if !ok { - return nil, fmt.Errorf("unexpected list response type %T", k) - } + switch { + // this path contains a tail wildcard, attempt to walk the tree + case strings.HasSuffix(path, "/*"): + t, err := fetchTemplatesFromTree(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...) - for _, key := range keys { - t, err := traverseSecrets(ctx, client, paths.Join(path, key.(string)), pathMount, v2) + // don't allow any other wildcards + case strings.Contains(path, "*"): + return nil, fmt.Errorf("the path %q cannot contain '*' wildcard characters except as the last element of the path", path) + + // regular secret path + default: + t, err := fetchTemplatesFromSecret(ctx, client, path, mountPath, v2) if err != nil { - return nil, err + return nil, fmt.Errorf("could not read secret at %q: %v", path, err) } templates = append(templates, t...) } - } else { - t, err := readSecret(ctx, client, path, pathMount, v2) + } + + return templates, nil +} + +func fetchTemplatesFromTree(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { + var templates []generatedConfigEnvTemplate + + if v2 { + path = addPrefixToKVPath(path, mountPath, "metadata", true) + } + + err := walkSecretsTree(ctx, client, path, func(child string, directory bool) error { + if directory { + return nil + } + + t, err := fetchTemplatesFromSecret(ctx, client, child, mountPath, v2) if err != nil { - return nil, err + return err } templates = append(templates, t...) + + return nil + }) + + if err != nil { + return nil, err } return templates, nil } -func readSecret(ctx context.Context, client *api.Client, path, pathMount string, v2 bool) ([]*config.EnvTemplateGen, error) { - var templates []*config.EnvTemplateGen +func fetchTemplatesFromSecret(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { + var templates []generatedConfigEnvTemplate if v2 { - path = addPrefixToKVPath(path, pathMount, "data", true) + path = addPrefixToKVPath(path, mountPath, "data", true) } resp, err := client.Logical().ReadWithContext(ctx, path) @@ -261,31 +260,37 @@ func readSecret(ctx context.Context, client *api.Client, path, pathMount string, return nil, fmt.Errorf("secret not found") } - data := resp.Data + 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 } - var fields []string + 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 { - v2AdjustedField := field - if v2 { - v2AdjustedField = "data." + field - } - templates = append(templates, &config.EnvTemplateGen{ + templates = append(templates, generatedConfigEnvTemplate{ Name: constructDefaultEnvironmentKey(path, field), - Contents: fmt.Sprintf(`{{ with secret "%s" }}{{ .Data.%s }}{{ end }}`, path, v2AdjustedField), + Contents: fmt.Sprintf(`{{ with secret "%s" }}{{ %s.%s }}{{ end }}`, path, dataContents, field), ErrorOnMissingKey: true, }) } @@ -305,5 +310,45 @@ func constructDefaultEnvironmentKey(path string, field string) string { 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 config generation. +type generatedConfig struct { + AutoAuth *generatedConfigAutoAuth `hcl:"auto_auth,block"` + Vault *generatedConfigVault `hcl:"vault,block"` + EnvTemplates []generatedConfigEnvTemplate `hcl:"env_template,block"` + Exec *generatedConfigExec `hcl:"exec,block"` +} + +type generatedConfigExec struct { + Command []string `hcl:"command"` + RestartOnSecretChanges bool `hcp:"restart_on_secret_changes"` + RestartKillSignal string `hcp:"restart_kill_signal"` +} + +type generatedConfigEnvTemplate struct { + Name string `hcl:"name,label"` + Contents string `hcl:"contents,attr"` + ErrorOnMissingKey bool `hcl:"error_on_missing_key,optional"` +} + +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"` } diff --git a/command/commands.go b/command/commands.go index 40fd57963e79..f89d05654abb 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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(), From 647843ca2dce8e0a4d8ec6506e4b9dd830e60da7 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 11:43:42 -0400 Subject: [PATCH 03/21] tests --- command/agent_generate_config.go | 105 +++++++++------ command/agent_generate_config_test.go | 184 ++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 37 deletions(-) create mode 100644 command/agent_generate_config_test.go diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 8d77d5a13bab..716187d913ab 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -7,9 +7,10 @@ import ( "context" "fmt" "os" - "regexp" + paths "path" "sort" "strings" + "unicode" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclwrite" @@ -27,9 +28,9 @@ var ( type AgentGenerateConfigCommand struct { *BaseCommand - flagType string - flagSecrets []string - flagExec string + flagType string + flagPaths []string + flagExec string } func (c *AgentGenerateConfigCommand) Synopsis() string { @@ -62,8 +63,8 @@ func (c *AgentGenerateConfigCommand) Flags() *FlagSets { }) f.StringSliceVar(&StringSliceVar{ - Name: "secret", - Target: &c.flagSecrets, + 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(), }) @@ -109,7 +110,7 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { return 2 } - templates, err := fetchTemplates(ctx, client, c.flagSecrets) + templates, err := constructTemplates(ctx, client, c.flagPaths) if err != nil { c.UI.Error(fmt.Sprintf("Error generating templates: %v", err)) return 2 @@ -129,21 +130,25 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { } config := generatedConfig{ - Vault: &generatedConfigVault{ - Address: client.Address(), - }, - AutoAuth: &generatedConfigAutoAuth{ - Method: &generatedConfigAutoAuthMethod{ + AutoAuth: generatedConfigAutoAuth{ + Method: generatedConfigAutoAuthMethod{ Type: "token_file", Config: generatedConfigAutoAuthMethodConfig{ TokenFilePath: tokenPath, }, }, }, + TemplateConfig: generatedConfigTemplateConfig{ + StaticSecretRendereInterval: "30s", + ExitOnRetryFailure: true, + }, + Vault: generatedConfigVault{ + Address: client.Address(), + }, EnvTemplates: templates, - Exec: &generatedConfigExec{ + Exec: generatedConfigExec{ Command: execCommand, - RestartOnSecretChanges: true, + RestartOnSecretChanges: "always", RestartKillSignal: "SIGTERM", }, } @@ -180,10 +185,10 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { return 0 } -func fetchTemplates(ctx context.Context, client *api.Client, secretPaths []string) ([]generatedConfigEnvTemplate, error) { +func constructTemplates(ctx context.Context, client *api.Client, paths []string) ([]generatedConfigEnvTemplate, error) { var templates []generatedConfigEnvTemplate - for _, path := range secretPaths { + for _, path := range paths { path = sanitizePath(path) mountPath, v2, err := isKVv2(path, client) @@ -192,21 +197,21 @@ func fetchTemplates(ctx context.Context, client *api.Client, secretPaths []strin } switch { - // this path contains a tail wildcard, attempt to walk the tree case strings.HasSuffix(path, "/*"): - t, err := fetchTemplatesFromTree(ctx, client, path[:len(path)-2], mountPath, v2) + // 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...) - // don't allow any other wildcards 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) - // regular secret path default: - t, err := fetchTemplatesFromSecret(ctx, client, path, mountPath, v2) + // 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) } @@ -217,11 +222,21 @@ func fetchTemplates(ctx context.Context, client *api.Client, secretPaths []strin return templates, nil } -func fetchTemplatesFromTree(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { +func constructTemplatesFromTree(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { var templates []generatedConfigEnvTemplate if v2 { - path = addPrefixToKVPath(path, mountPath, "metadata", true) + 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 { @@ -229,7 +244,14 @@ func fetchTemplatesFromTree(ctx context.Context, client *api.Client, path, mount return nil } - t, err := fetchTemplatesFromSecret(ctx, client, child, mountPath, v2) + 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 } @@ -245,7 +267,7 @@ func fetchTemplatesFromTree(ctx context.Context, client *api.Client, path, mount return templates, nil } -func fetchTemplatesFromSecret(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { +func constructTemplatesFromSecret(ctx context.Context, client *api.Client, path, mountPath string, v2 bool) ([]generatedConfigEnvTemplate, error) { var templates []generatedConfigEnvTemplate if v2 { @@ -302,10 +324,12 @@ func constructDefaultEnvironmentKey(path string, field string) string { pathParts := strings.Split(path, "/") pathPartsLast := pathParts[len(pathParts)-1] - nonWordRegex := regexp.MustCompile(`[^\w]+`) // match a sequence of non-word characters + notLetterOrNumber := func(r rune) bool { + return !unicode.IsLetter(r) && !unicode.IsNumber(r) + } - p1 := nonWordRegex.Split(pathPartsLast, -1) - p2 := nonWordRegex.Split(field, -1) + p1 := strings.FieldsFunc(pathPartsLast, notLetterOrNumber) + p2 := strings.FieldsFunc(field, notLetterOrNumber) keyParts := append(p1, p2...) @@ -316,24 +340,31 @@ func constructDefaultEnvironmentKey(path string, field string) string { // 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 config generation. +// should not be used for anything other than generation. + type generatedConfig struct { - AutoAuth *generatedConfigAutoAuth `hcl:"auto_auth,block"` - Vault *generatedConfigVault `hcl:"vault,block"` - EnvTemplates []generatedConfigEnvTemplate `hcl:"env_template,block"` - Exec *generatedConfigExec `hcl:"exec,block"` + 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 { + StaticSecretRendereInterval string `hcl:"static_secret_render_interval"` + ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"` } type generatedConfigExec struct { Command []string `hcl:"command"` - RestartOnSecretChanges bool `hcp:"restart_on_secret_changes"` - RestartKillSignal string `hcp:"restart_kill_signal"` + RestartOnSecretChanges string `hcl:"restart_on_secret_changes"` + RestartKillSignal string `hcl:"restart_kill_signal"` } type generatedConfigEnvTemplate struct { Name string `hcl:"name,label"` Contents string `hcl:"contents,attr"` - ErrorOnMissingKey bool `hcl:"error_on_missing_key,optional"` + ErrorOnMissingKey bool `hcl:"error_on_missing_key"` } type generatedConfigVault struct { @@ -341,7 +372,7 @@ type generatedConfigVault struct { } type generatedConfigAutoAuth struct { - Method *generatedConfigAutoAuthMethod `hcl:"method,block"` + Method generatedConfigAutoAuthMethod `hcl:"method,block"` } type generatedConfigAutoAuthMethod struct { diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go new file mode 100644 index 000000000000..19820089a5cc --- /dev/null +++ b/command/agent_generate_config_test.go @@ -0,0 +1,184 @@ +package command + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/hashicorp/vault/api" +) + +// TestConstructTemplates tests the construcTemplates helper function +func TestConstructTemplates(t *testing.T) { + // test setup + client, closer := testVaultServer(t) + defer closer() + + // enable kv-v1 backend + if err := client.Sys().Mount("kv-v1/", &api.MountInput{ + Type: "kv-v1", + }); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + // enable kv-v2 backend + if err := client.Sys().Mount("kv-v2/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelContextFunc() + + // populate 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 { + t.Fatal(err) + } + + if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{ + "user": "test", + "password": "Hashi123", + }); err != nil { + t.Fatal(err) + } + } + + // tests + 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 { + 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) + } + } + }) + } +} From 7a8f1d5a9139f77b5f596a4b5ae1dbe4434acbdd Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 11:49:51 -0400 Subject: [PATCH 04/21] changelog --- changelog/20530.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/20530.txt diff --git a/changelog/20530.txt b/changelog/20530.txt new file mode 100644 index 000000000000..fa52489e1545 --- /dev/null +++ b/changelog/20530.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Add 'agent generate-config' sub-command +``` From 2533835ed0a280084115369bc1ce76aa1c02ee20 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 11:54:47 -0400 Subject: [PATCH 05/21] fmt --- command/agent_generate_config.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 716187d913ab..0db0afbb3285 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -259,7 +259,6 @@ func constructTemplatesFromTree(ctx context.Context, client *api.Client, path, m return nil }) - if err != nil { return nil, err } From a3e31721b19ffedec732c8183d5859f82bc6ec49 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 12:27:02 -0400 Subject: [PATCH 06/21] copyright note --- command/agent_generate_config_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index 19820089a5cc..11d543f0a7ac 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package command import ( From 8429aab8fe51bddd74e74f2796c7a1eb08f9d31b Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 12:34:24 -0400 Subject: [PATCH 07/21] feature --- changelog/20530.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/20530.txt b/changelog/20530.txt index fa52489e1545..dda524034dbf 100644 --- a/changelog/20530.txt +++ b/changelog/20530.txt @@ -1,3 +1,3 @@ -```release-note:improvement +```release-note:feature cli: Add 'agent generate-config' sub-command ``` From 74aed7184681a90fe88909024c7c440d60c789dd Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 12:44:40 -0400 Subject: [PATCH 08/21] -type is now required --- command/agent_generate_config.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 0db0afbb3285..dae9ea23e117 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -53,10 +53,9 @@ func (c *AgentGenerateConfigCommand) Flags() *FlagSets { f := set.NewFlagSet("Command Options") f.StringVar(&StringVar{ - Name: "type", - Target: &c.flagType, - Default: "env-template", - Usage: "Type of configuration file to generate; currently, only 'env-template' is supported.", + Name: "type", + Target: &c.flagType, + Usage: "Type of configuration file to generate; currently, only 'env-template' is supported.", Completion: complete.PredictSet( "env-template", ), @@ -104,6 +103,16 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { 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()) From 12ec3699e89e3885a9307413a9b66f70aa44caae Mon Sep 17 00:00:00 2001 From: Anton Averchenkov <84287187+averche@users.noreply.github.com> Date: Fri, 5 May 2023 13:13:06 -0400 Subject: [PATCH 09/21] Update command/agent_generate_config_test.go --- command/agent_generate_config_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index 11d543f0a7ac..de7319653311 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -24,7 +24,6 @@ func TestConstructTemplates(t *testing.T) { }); err != nil { t.Fatal(err) } - time.Sleep(time.Second) // enable kv-v2 backend if err := client.Sys().Mount("kv-v2/", &api.MountInput{ From 1eeb8d7392edf07a13354afb67dbf272e8b1ad9a Mon Sep 17 00:00:00 2001 From: Anton Averchenkov <84287187+averche@users.noreply.github.com> Date: Fri, 5 May 2023 13:13:17 -0400 Subject: [PATCH 10/21] Update command/agent_generate_config_test.go --- command/agent_generate_config_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index de7319653311..ee74a1bcaa16 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -31,7 +31,6 @@ func TestConstructTemplates(t *testing.T) { }); err != nil { t.Fatal(err) } - time.Sleep(time.Second) ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second) defer cancelContextFunc() From 8027803faa9d5ec7107dd5f2b81e19364edc7484 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov <84287187+averche@users.noreply.github.com> Date: Fri, 5 May 2023 13:13:26 -0400 Subject: [PATCH 11/21] Update command/agent_generate_config.go --- command/agent_generate_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index dae9ea23e117..bc0fc471c9d2 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -148,7 +148,7 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { }, }, TemplateConfig: generatedConfigTemplateConfig{ - StaticSecretRendereInterval: "30s", + StaticSecretRendereInterval: "5m", ExitOnRetryFailure: true, }, Vault: generatedConfigVault{ From 20107dc409abf453811c406502521e7e94de3ec9 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov <84287187+averche@users.noreply.github.com> Date: Fri, 5 May 2023 13:17:34 -0400 Subject: [PATCH 12/21] Update command/agent_generate_config.go Co-authored-by: Daniel Huckins --- command/agent_generate_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index bc0fc471c9d2..1a8a0ba42e8d 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -359,7 +359,7 @@ type generatedConfig struct { } type generatedConfigTemplateConfig struct { - StaticSecretRendereInterval string `hcl:"static_secret_render_interval"` + StaticSecretRenderInterval string `hcl:"static_secret_render_interval"` ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"` } From 46b23204bc8691d01d19ea9f5a457dc19ad33d6a Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 13:20:41 -0400 Subject: [PATCH 13/21] fix build --- command/agent_generate_config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 1a8a0ba42e8d..9b756172f0c3 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -148,8 +148,8 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { }, }, TemplateConfig: generatedConfigTemplateConfig{ - StaticSecretRendereInterval: "5m", - ExitOnRetryFailure: true, + StaticSecretRenderInterval: "5m", + ExitOnRetryFailure: true, }, Vault: generatedConfigVault{ Address: client.Address(), @@ -360,7 +360,7 @@ type generatedConfig struct { type generatedConfigTemplateConfig struct { StaticSecretRenderInterval string `hcl:"static_secret_render_interval"` - ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"` + ExitOnRetryFailure bool `hcl:"exit_on_retry_failure"` } type generatedConfigExec struct { From 2551557a78f74fafc320f6e1c844c81b6db19455 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 5 May 2023 13:35:22 -0400 Subject: [PATCH 14/21] paralell tests --- command/agent_generate_config_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index ee74a1bcaa16..e8f3d891633c 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -164,7 +164,11 @@ func TestConstructTemplates(t *testing.T) { } for name, tc := range cases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + templates, err := constructTemplates(ctx, client, tc.paths) if tc.expectedError { From 159c8136e3d8e1c74825e7e5eabf66c34cbd1458 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Mon, 8 May 2023 17:13:00 -0400 Subject: [PATCH 15/21] refactor generateConfiguration --- command/agent_generate_config.go | 77 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 9b756172f0c3..a53eebd0cb25 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -6,6 +6,7 @@ package command import ( "context" "fmt" + "io" "os" paths "path" "sort" @@ -87,8 +88,6 @@ func (c *AgentGenerateConfigCommand) AutocompleteFlags() complete.Flags { } func (c *AgentGenerateConfigCommand) Run(args []string) int { - ctx := context.Background() - flags := c.Flags() if err := flags.Parse(args); err != nil { @@ -119,23 +118,56 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { return 2 } - templates, err := constructTemplates(ctx, client, c.flagPaths) + config, err := generateConfiguration(context.Background(), client, c.flagExec, c.flagPaths) if err != nil { - c.UI.Error(fmt.Sprintf("Error generating templates: %v", err)) + 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)) + + return 0 +} + +func generateConfiguration(ctx context.Context, client *api.Client, flagExec string, flagPaths []string) (io.WriterTo, error) { var execCommand []string - if c.flagExec != "" { - execCommand = strings.Split(c.flagExec, " ") + if flagExec != "" { + execCommand = strings.Split(flagExec, " ") } else { execCommand = []string{"env"} } tokenPath, err := homedir.Expand("~/.vault-token") if err != nil { - c.UI.Error(fmt.Sprintf("Could not expand home directory: %v", err)) - return 2 + 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{ @@ -154,44 +186,19 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { Vault: generatedConfigVault{ Address: client.Address(), }, - EnvTemplates: templates, Exec: generatedConfigExec{ Command: execCommand, RestartOnSecretChanges: "always", RestartKillSignal: "SIGTERM", }, - } - - var configPath string - if len(args) == 1 { - configPath = args[0] - } else { - configPath = "agent.hcl" + EnvTemplates: templates, } contents := hclwrite.NewEmptyFile() gohcl.EncodeIntoBody(&config, contents.Body()) - f, err := os.Create(configPath) - if err != nil { - c.UI.Error(fmt.Sprintf("Could not create configuration file %q: %v", configPath, err)) - return 1 - } - defer func() { - if err := f.Close(); err != nil { - c.UI.Error(fmt.Sprintf("Could not close configuration file %q: %v", configPath, err)) - } - }() - - if _, err := contents.WriteTo(f); err != nil { - c.UI.Error(fmt.Sprintf("Could not write to configuration file %q: %v", configPath, err)) - return 1 - } - - c.UI.Info(fmt.Sprintf("Successfully generated %q configuration file!", configPath)) - - return 0 + return contents, nil } func constructTemplates(ctx context.Context, client *api.Client, paths []string) ([]generatedConfigEnvTemplate, error) { From 1f83221aacff779fbd68c9d8f1bf7f185f13ab33 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Mon, 8 May 2023 20:34:38 -0400 Subject: [PATCH 16/21] add generateConfig test --- command/agent_generate_config_test.go | 179 +++++++++++++++++++------- command/command_test.go | 44 +++++++ 2 files changed, 179 insertions(+), 44 deletions(-) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index e8f3d891633c..2e50fc2279b6 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -4,60 +4,22 @@ package command import ( + "bytes" "context" "reflect" + "regexp" "testing" "time" - - "github.com/hashicorp/vault/api" ) // TestConstructTemplates tests the construcTemplates helper function func TestConstructTemplates(t *testing.T) { - // test setup - client, closer := testVaultServer(t) - defer closer() - - // enable kv-v1 backend - if err := client.Sys().Mount("kv-v1/", &api.MountInput{ - Type: "kv-v1", - }); err != nil { - t.Fatal(err) - } - - // enable kv-v2 backend - if err := client.Sys().Mount("kv-v2/", &api.MountInput{ - Type: "kv-v2", - }); err != nil { - t.Fatal(err) - } - ctx, cancelContextFunc := context.WithTimeout(context.Background(), 5*time.Second) defer cancelContextFunc() - // populate 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 { - t.Fatal(err) - } - - if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{ - "user": "test", - "password": "Hashi123", - }); err != nil { - t.Fatal(err) - } - } + client, closer := testVaultServerWithSecrets(ctx, t) + defer closer() - // tests cases := map[string]struct { paths []string expected []generatedConfigEnvTemplate @@ -167,8 +129,6 @@ func TestConstructTemplates(t *testing.T) { name, tc := name, tc t.Run(name, func(t *testing.T) { - t.Parallel() - templates, err := constructTemplates(ctx, client, tc.paths) if tc.expectedError { @@ -187,3 +147,134 @@ func TestConstructTemplates(t *testing.T) { }) } } + +// 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_kill_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_kill_signal = "SIGTERM" +} +`), + expectedError: false, + }, + "kv-v2-simple": { + flagExec: "./my-app arg1 arg2", + flagPaths: []string{"kv-v2/foo"}, + expected: regexp.MustCompile(``), + 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()) + } + } + }) + } +} diff --git a/command/command_test.go b/command/command_test.go index 73719d583c2f..23911d5dfc02 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -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() From 5a9e550fddf3bf41c36c585d5a75c6ff08cb6e84 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Mon, 8 May 2023 20:42:57 -0400 Subject: [PATCH 17/21] add warning --- command/agent_generate_config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index a53eebd0cb25..0f3e62924b53 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -149,6 +149,8 @@ func (c *AgentGenerateConfigCommand) Run(args []string) int { 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 } From a5d9cf2785ee33356c6789d7b6cfe22ff0488cb3 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Thu, 11 May 2023 12:25:59 -0400 Subject: [PATCH 18/21] remove empty test --- command/agent_generate_config_test.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index 2e50fc2279b6..7a4e474494d2 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -8,6 +8,7 @@ import ( "context" "reflect" "regexp" + "strings" "testing" "time" ) @@ -245,12 +246,6 @@ exec \{ `), expectedError: false, }, - "kv-v2-simple": { - flagExec: "./my-app arg1 arg2", - flagPaths: []string{"kv-v2/foo"}, - expected: regexp.MustCompile(``), - expectedError: false, - }, } for name, tc := range cases { From b5732e5bebf98aab7891336bbe745395d784e933 Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Tue, 16 May 2023 17:25:46 -0400 Subject: [PATCH 19/21] fix build --- command/agent_generate_config_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index 7a4e474494d2..1f296b3b55a3 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -8,7 +8,6 @@ import ( "context" "reflect" "regexp" - "strings" "testing" "time" ) From baa3ec5104116efe66d7abd212142d5de1ed246e Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 19 May 2023 11:45:43 -0400 Subject: [PATCH 20/21] rename to restart_stop_signal --- command/agent_generate_config.go | 4 ++-- command/agent_generate_config_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go index 0f3e62924b53..51512cd92ca8 100644 --- a/command/agent_generate_config.go +++ b/command/agent_generate_config.go @@ -191,7 +191,7 @@ func generateConfiguration(ctx context.Context, client *api.Client, flagExec str Exec: generatedConfigExec{ Command: execCommand, RestartOnSecretChanges: "always", - RestartKillSignal: "SIGTERM", + RestartStopSignal: "SIGTERM", }, EnvTemplates: templates, } @@ -375,7 +375,7 @@ type generatedConfigTemplateConfig struct { type generatedConfigExec struct { Command []string `hcl:"command"` RestartOnSecretChanges string `hcl:"restart_on_secret_changes"` - RestartKillSignal string `hcl:"restart_kill_signal"` + RestartStopSignal string `hcl:"restart_stop_signal"` } type generatedConfigEnvTemplate struct { diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index 1f296b3b55a3..7e9ef963a0df 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -198,7 +198,7 @@ env_template "FOO_USER" \{ exec \{ command = \["./my-app", "arg1", "arg2"\] restart_on_secret_changes = "always" - restart_kill_signal = "SIGTERM" + restart_stop_signal = "SIGTERM" } `), expectedError: false, From c8225ba41b78329eb7c426640a51ead40d114b3b Mon Sep 17 00:00:00 2001 From: Anton Averchenkov Date: Fri, 19 May 2023 11:50:45 -0400 Subject: [PATCH 21/21] fix test --- command/agent_generate_config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go index 7e9ef963a0df..f225a7c9e8cb 100644 --- a/command/agent_generate_config_test.go +++ b/command/agent_generate_config_test.go @@ -240,7 +240,7 @@ env_template "FOO_USER" \{ exec \{ command = \["env"\] restart_on_secret_changes = "always" - restart_kill_signal = "SIGTERM" + restart_stop_signal = "SIGTERM" } `), expectedError: false,