From f551f4e5baf0566dfea4f1ccf951e94742d9a68b Mon Sep 17 00:00:00 2001 From: Anton Averchenkov <84287187+averche@users.noreply.github.com> Date: Fri, 19 May 2023 13:42:19 -0400 Subject: [PATCH] cli: Add 'agent generate-config' sub-command (#20530) --- changelog/20530.txt | 3 + command/agent_generate_config.go | 402 ++++++++++++++++++++++++++ command/agent_generate_config_test.go | 274 ++++++++++++++++++ command/command_test.go | 44 +++ command/commands.go | 5 + go.mod | 4 + go.sum | 9 + 7 files changed, 741 insertions(+) create mode 100644 changelog/20530.txt create mode 100644 command/agent_generate_config.go create mode 100644 command/agent_generate_config_test.go diff --git a/changelog/20530.txt b/changelog/20530.txt new file mode 100644 index 000000000..dda524034 --- /dev/null +++ b/changelog/20530.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli: Add 'agent generate-config' sub-command +``` diff --git a/command/agent_generate_config.go b/command/agent_generate_config.go new file mode 100644 index 000000000..51512cd92 --- /dev/null +++ b/command/agent_generate_config.go @@ -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"` +} diff --git a/command/agent_generate_config_test.go b/command/agent_generate_config_test.go new file mode 100644 index 000000000..f225a7c9e --- /dev/null +++ b/command/agent_generate_config_test.go @@ -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()) + } + } + }) + } +} diff --git a/command/command_test.go b/command/command_test.go index a34603a8d..f1a5269b9 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() diff --git a/command/commands.go b/command/commands.go index a3b023e67..c3867dc16 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(), diff --git a/go.mod b/go.mod index fa2ce46e6..f028c62ed 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 90c25af40..da81b58e5 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=