diff --git a/command/flags/flag_map_value.go b/command/flags/flag_map_value.go index aee640b978..8a8c3b6bd9 100644 --- a/command/flags/flag_map_value.go +++ b/command/flags/flag_map_value.go @@ -35,3 +35,15 @@ func (h *FlagMapValue) Set(value string) error { return nil } + +// Merge will overlay this value if it has been set. +func (h *FlagMapValue) Merge(onto map[string]string) { + if h == nil || onto == nil { + return + } + for k, v := range *h { + if _, ok := onto[k]; !ok { + onto[k] = v + } + } +} diff --git a/command/flags/flag_map_value_test.go b/command/flags/flag_map_value_test.go index cf582c44a2..4348d17961 100644 --- a/command/flags/flag_map_value_test.go +++ b/command/flags/flag_map_value_test.go @@ -3,6 +3,8 @@ package flags import ( "fmt" "testing" + + "github.com/stretchr/testify/require" ) func TestFlagMapValueSet(t *testing.T) { @@ -78,3 +80,75 @@ func TestFlagMapValueSet(t *testing.T) { } }) } + +func TestFlagMapValueMerge(t *testing.T) { + cases := map[string]struct { + src FlagMapValue + dst map[string]string + exp map[string]string + }{ + "empty source and destination": {}, + "empty source": { + dst: map[string]string{ + "key": "val", + }, + exp: map[string]string{ + "key": "val", + }, + }, + "empty destination": { + src: map[string]string{ + "key": "val", + }, + dst: make(map[string]string), + exp: map[string]string{ + "key": "val", + }, + }, + "non-overlapping keys": { + src: map[string]string{ + "key1": "val1", + }, + dst: map[string]string{ + "key2": "val2", + }, + exp: map[string]string{ + "key1": "val1", + "key2": "val2", + }, + }, + "overlapping keys": { + src: map[string]string{ + "key1": "val1", + }, + dst: map[string]string{ + "key1": "val2", + }, + exp: map[string]string{ + "key1": "val2", + }, + }, + "multiple keys": { + src: map[string]string{ + "key1": "val1", + "key2": "val2", + }, + dst: map[string]string{ + "key1": "val2", + "key3": "val3", + }, + exp: map[string]string{ + "key1": "val2", + "key2": "val2", + "key3": "val3", + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + c.src.Merge(c.dst) + require.Equal(t, c.exp, c.dst) + }) + } +} diff --git a/lib/retry/retry.go b/lib/retry/retry.go index 72ea79afa5..b76b10e532 100644 --- a/lib/retry/retry.go +++ b/lib/retry/retry.go @@ -2,6 +2,7 @@ package retry import ( "context" + "fmt" "math/rand" "time" ) @@ -30,7 +31,7 @@ func NewJitter(percent int64) Jitter { } // Waiter records the number of failures and performs exponential backoff when -// when there are consecutive failures. +// there are consecutive failures. type Waiter struct { // MinFailures before exponential backoff starts. Any failures before // MinFailures is reached will wait MinWait time. @@ -117,3 +118,23 @@ func (w *Waiter) Wait(ctx context.Context) error { func (w *Waiter) NextWait() time.Duration { return w.delay() } + +// RetryLoop retries an operation until either operation completes without error +// or Waiter's context is canceled. +func (w *Waiter) RetryLoop(ctx context.Context, operation func() error) error { + var lastError error + for { + if err := w.Wait(ctx); err != nil { + // The error will only be non-nil if the context is canceled. + return fmt.Errorf("could not retry operation: %w", lastError) + } + + if err := operation(); err == nil { + // Reset the failure count seen by the waiter if there was no error. + w.Reset() + return nil + } else { + lastError = err + } + } +} diff --git a/lib/retry/retry_test.go b/lib/retry/retry_test.go index 8c4f664f69..470d0db66e 100644 --- a/lib/retry/retry_test.go +++ b/lib/retry/retry_test.go @@ -2,6 +2,7 @@ package retry import ( "context" + "fmt" "math" "testing" "time" @@ -157,6 +158,38 @@ func TestWaiter_Wait(t *testing.T) { }) } +func TestWaiter_RetryLoop(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + // Change the default factor so that we retry faster. + w := &Waiter{Factor: 1 * time.Millisecond} + + t.Run("exits if operation is successful after a few reties", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + numRetries := 0 + err := w.RetryLoop(ctx, func() error { + if numRetries < 2 { + numRetries++ + return fmt.Errorf("operation not successful") + } + return nil + }) + require.NoError(t, err) + }) + + t.Run("errors if operation is never successful", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + t.Cleanup(cancel) + err := w.RetryLoop(ctx, func() error { + return fmt.Errorf("operation not successful") + }) + require.NotNil(t, err) + require.EqualError(t, err, "could not retry operation: operation not successful") + }) +} + func runWait(ctx context.Context, w *Waiter) (time.Duration, error) { before := time.Now() err := w.Wait(ctx) diff --git a/website/content/commands/snapshot/agent.mdx b/website/content/commands/snapshot/agent.mdx index 7607fc2b51..b82152f97e 100644 --- a/website/content/commands/snapshot/agent.mdx +++ b/website/content/commands/snapshot/agent.mdx @@ -143,6 +143,12 @@ Usage: `consul snapshot agent [options]` "key_file": "", "license_path": "", "tls_server_name": "", + "login": { + "auth_method": "", + "bearer_token": "", + "bearer_token_file": "", + "meta": {}, + }, "log": { "level": "INFO", "enable_syslog": false, @@ -238,6 +244,16 @@ if desired. - `-syslog-facility` - Sets the facility to use for forwarding logs to syslog. Defaults to "LOCAL0". +- `login-auth-method` - Auth method name to use to log into Consul. If provided, the token obtained with this auth method + will be used instead of a static token if it is provided. Currently, only `kubernetes` auth method type is supported. + +- `login-bearer-token` - Bearer token to use to log into Consul. Used only if `-login-auth-method` is set. + +- `login-bearer-token-file` - A file container bearer token to use for logging into Consul. + `-login-bearer-token` is ignored if this flag is provided. + +- `login-meta` - Metadata to set on the token, formatted as key=value. This flag may be provided multiple times. + #### Local Storage Options - `-local-path` - Location to store snapshots locally. The default behavior @@ -277,7 +293,7 @@ Note that despite the AWS references, any S3-compatible endpoint can be specifie Use this if you want to rely on [S3's versioning capabilities](http://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html) instead of the agent handling it for you. - `-aws-s3-force-path-style` - Enables the use of legacy path-based addressing instead of virtual addressing. This flag is required by minio - and other 3rd party S3 compatible object storage platforms where DNS or TLS requirements for virtual addressing are prohibitive. + and other 3rd party S3 compatible object storage platforms where DNS or TLS requirements for virtual addressing are prohibitive. For more information, refer to the AWS documentation on [Methods for accessing a bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html) - `-aws-s3-enable-kms` - Enables using [Amazon KMS](https://aws.amazon.com/kms/) for encrypting snapshots. @@ -394,6 +410,6 @@ then the order of precedence is as follows: 2. `CONSUL_LICENSE_PATH` variable 3. `license_path` configuration. -The ability to load licenses from the configuration or environment was added in v1.10.0, -v1.9.7 and v1.8.13. See the [licensing documentation](/docs/enterprise/license/overview) for +The ability to load licenses from the configuration or environment was added in v1.10.0, +v1.9.7 and v1.8.13. See the [licensing documentation](/docs/enterprise/license/overview) for more information about Consul Enterprise license management.