| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package logical |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "strings" |
| |
| "github.com/hashicorp/errwrap" |
| "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/vault/sdk/helper/jsonutil" |
| ) |
| |
| // ErrReadOnly is returned when a backend does not support |
| // writing. This can be caused by a read-only replica or secondary |
| // cluster operation. |
| var ErrReadOnly = errors.New("cannot write to readonly storage") |
| |
| // ErrSetupReadOnly is returned when a write operation is attempted on a |
| // storage while the backend is still being setup. |
| var ErrSetupReadOnly = errors.New("cannot write to storage during setup") |
| |
| // Plugins using Paths.WriteForwardedStorage will need to use this sentinel |
| // in their path to write cross-cluster. See the description of that parameter |
| // for more information. |
| const PBPWFClusterSentinel = "{{clusterId}}" |
| |
| // Storage is the way that logical backends are able read/write data. |
| type Storage interface { |
| List(context.Context, string) ([]string, error) |
| Get(context.Context, string) (*StorageEntry, error) |
| Put(context.Context, *StorageEntry) error |
| Delete(context.Context, string) error |
| } |
| |
| // StorageEntry is the entry for an item in a Storage implementation. |
| type StorageEntry struct { |
| Key string |
| Value []byte |
| SealWrap bool |
| } |
| |
| // DecodeJSON decodes the 'Value' present in StorageEntry. |
| func (e *StorageEntry) DecodeJSON(out interface{}) error { |
| return jsonutil.DecodeJSON(e.Value, out) |
| } |
| |
| // StorageEntryJSON creates a StorageEntry with a JSON-encoded value. |
| func StorageEntryJSON(k string, v interface{}) (*StorageEntry, error) { |
| encodedBytes, err := jsonutil.EncodeJSON(v) |
| if err != nil { |
| return nil, errwrap.Wrapf("failed to encode storage entry: {{err}}", err) |
| } |
| |
| return &StorageEntry{ |
| Key: k, |
| Value: encodedBytes, |
| }, nil |
| } |
| |
| type ClearableView interface { |
| List(context.Context, string) ([]string, error) |
| Delete(context.Context, string) error |
| } |
| |
| // ScanView is used to scan all the keys in a view iteratively |
| func ScanView(ctx context.Context, view ClearableView, cb func(path string)) error { |
| frontier := []string{""} |
| for len(frontier) > 0 { |
| n := len(frontier) |
| current := frontier[n-1] |
| frontier = frontier[:n-1] |
| |
| // List the contents |
| contents, err := view.List(ctx, current) |
| if err != nil { |
| return errwrap.Wrapf(fmt.Sprintf("list failed at path %q: {{err}}", current), err) |
| } |
| |
| // Handle the contents in the directory |
| for _, c := range contents { |
| // Exit if the context has been canceled |
| if ctx.Err() != nil { |
| return ctx.Err() |
| } |
| fullPath := current + c |
| if strings.HasSuffix(c, "/") { |
| frontier = append(frontier, fullPath) |
| } else { |
| cb(fullPath) |
| } |
| } |
| } |
| return nil |
| } |
| |
| // CollectKeys is used to collect all the keys in a view |
| func CollectKeys(ctx context.Context, view ClearableView) ([]string, error) { |
| return CollectKeysWithPrefix(ctx, view, "") |
| } |
| |
| // CollectKeysWithPrefix is used to collect all the keys in a view with a given prefix string |
| func CollectKeysWithPrefix(ctx context.Context, view ClearableView, prefix string) ([]string, error) { |
| var keys []string |
| |
| cb := func(path string) { |
| if strings.HasPrefix(path, prefix) { |
| keys = append(keys, path) |
| } |
| } |
| |
| // Scan for all the keys |
| if err := ScanView(ctx, view, cb); err != nil { |
| return nil, err |
| } |
| return keys, nil |
| } |
| |
| // ClearView is used to delete all the keys in a view |
| func ClearView(ctx context.Context, view ClearableView) error { |
| return ClearViewWithLogging(ctx, view, nil) |
| } |
| |
| func ClearViewWithLogging(ctx context.Context, view ClearableView, logger hclog.Logger) error { |
| if view == nil { |
| return nil |
| } |
| |
| if logger == nil { |
| logger = hclog.NewNullLogger() |
| } |
| |
| // Collect all the keys |
| keys, err := CollectKeys(ctx, view) |
| if err != nil { |
| return err |
| } |
| |
| logger.Debug("clearing view", "total_keys", len(keys)) |
| |
| // Delete all the keys |
| var pctDone int |
| for idx, key := range keys { |
| // Rather than keep trying to do stuff with a canceled context, bail; |
| // storage will fail anyways |
| if ctx.Err() != nil { |
| return ctx.Err() |
| } |
| if err := view.Delete(ctx, key); err != nil { |
| return err |
| } |
| |
| newPctDone := idx * 100.0 / len(keys) |
| if int(newPctDone) > pctDone { |
| pctDone = int(newPctDone) |
| logger.Trace("view deletion progress", "percent", pctDone, "keys_deleted", idx) |
| } |
| } |
| |
| logger.Debug("view cleared") |
| |
| return nil |
| } |