cli: Add 'agent generate-config' sub-command (#20530)

This commit is contained in:
Anton Averchenkov 2023-05-19 13:42:19 -04:00 committed by GitHub
parent 92dc054bb3
commit f551f4e5ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 741 additions and 0 deletions

3
changelog/20530.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
cli: Add 'agent generate-config' sub-command
```

View 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"`
}

View 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())
}
}
})
}
}

View File

@ -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()

View File

@ -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(),

4
go.mod
View File

@ -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

9
go.sum
View File

@ -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=