github.com/hashicorp/vault/sdk@v0.13.0/helper/testhelpers/namespaces/namespaces.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package namespaces
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"path"
    11  	"slices"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/hashicorp/vault/api"
    16  )
    17  
    18  // RootNamespacePath is the path of the root namespace.
    19  const RootNamespacePath = ""
    20  
    21  // RootNamespaceID is the ID of the root namespace.
    22  const RootNamespaceID = "root"
    23  
    24  // ErrNotFound is returned by funcs in this package when something isn't found,
    25  // instead of returning (nil, nil).
    26  var ErrNotFound = errors.New("no namespaces found")
    27  
    28  // folderPath transforms an input path that refers to a namespace or mount point,
    29  // such that it adheres to the norms Vault prefers.  The result will have any
    30  // leading "/" stripped, and, except for the root namespace which is always
    31  // RootNamespacePath, will always end in a "/".
    32  func folderPath(path string) string {
    33  	if !strings.HasSuffix(path, "/") {
    34  		path += "/"
    35  	}
    36  
    37  	return strings.TrimPrefix(path, "/")
    38  }
    39  
    40  // joinPath concatenates its inputs using "/" as a delimiter.  The result will
    41  // adhere to folderPath conventions.
    42  func joinPath(s ...string) string {
    43  	return folderPath(path.Join(s...))
    44  }
    45  
    46  // GetNamespaceIDPaths does a namespace list and extracts the resulting paths
    47  // and namespace IDs, returning a map from namespace ID to path.  Returns
    48  // ErrNotFound if no namespaces exist beneath the current namespace set on the
    49  // client.
    50  func GetNamespaceIDPaths(client *api.Client) (map[string]string, error) {
    51  	secret, err := client.Logical().List("sys/namespaces")
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	if secret == nil {
    56  		return nil, ErrNotFound
    57  	}
    58  	if _, ok := secret.Data["key_info"]; !ok {
    59  		return nil, ErrNotFound
    60  	}
    61  
    62  	ret := map[string]string{}
    63  	for relNsPath, infoAny := range secret.Data["key_info"].(map[string]any) {
    64  		info := infoAny.(map[string]any)
    65  		id := info["id"].(string)
    66  		ret[id] = relNsPath
    67  	}
    68  	return ret, err
    69  }
    70  
    71  // WalkNamespaces does recursive namespace list commands to discover the complete
    72  // namespace hierarchy.  This may yield an error or inconsistent results if
    73  // namespaces change while we're querying them.
    74  // The callback f is invoked for every namespace discovered.  Namespace traversal
    75  // is pre-order depth-first. If f returns an error, traversal is aborted and the
    76  // error is returned.  Otherwise, an error is only returned if a request results
    77  // in an error.
    78  func WalkNamespaces(client *api.Client, f func(id, apiPath string) error) error {
    79  	return walkNamespacesRecursive(client, RootNamespaceID, RootNamespacePath, f)
    80  }
    81  
    82  func walkNamespacesRecursive(client *api.Client, startID, startApiPath string, f func(id, apiPath string) error) error {
    83  	if err := f(startID, startApiPath); err != nil {
    84  		return err
    85  	}
    86  
    87  	idpaths, err := GetNamespaceIDPaths(client.WithNamespace(startApiPath))
    88  	if err != nil {
    89  		if errors.Is(err, ErrNotFound) {
    90  			return nil
    91  		}
    92  		return err
    93  	}
    94  
    95  	for id, path := range idpaths {
    96  		fullPath := joinPath(startApiPath, path)
    97  
    98  		if err = walkNamespacesRecursive(client, id, fullPath, f); err != nil {
    99  			return err
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  // PollDeleteNamespace issues a namespace delete request and waits for it
   106  // to complete (since namespace deletes are asynchronous), at least until
   107  // ctx expires.
   108  func PollDeleteNamespace(ctx context.Context, client *api.Client, nsPath string) error {
   109  	_, err := client.Logical().Delete("sys/namespaces/" + nsPath)
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  LOOP:
   115  	for ctx.Err() == nil {
   116  		resp, err := client.Logical().Delete("sys/namespaces/" + nsPath)
   117  		if err != nil {
   118  			return err
   119  		}
   120  		for _, warn := range resp.Warnings {
   121  			if strings.HasPrefix(warn, "Namespace is already being deleted") {
   122  				time.Sleep(10 * time.Millisecond)
   123  				continue LOOP
   124  			}
   125  		}
   126  		break
   127  	}
   128  
   129  	return ctx.Err()
   130  }
   131  
   132  // DeleteAllNamespaces uses WalkNamespaces to delete all namespaces,
   133  // waiting for deletion to complete before returning.  The same caveats about
   134  // namespaces changing underneath us apply as in WalkNamespaces.
   135  // Traversal is depth-first pre-order, but we must do the deletion in the reverse
   136  // order, since a namespace containing namespaces cannot be deleted.
   137  func DeleteAllNamespaces(ctx context.Context, client *api.Client) error {
   138  	var nss []string
   139  	err := WalkNamespaces(client, func(id, apiPath string) error {
   140  		if apiPath != RootNamespacePath {
   141  			nss = append(nss, apiPath)
   142  		}
   143  		return nil
   144  	})
   145  	if err != nil {
   146  		return err
   147  	}
   148  	slices.Reverse(nss)
   149  	for _, apiPath := range nss {
   150  		if err := PollDeleteNamespace(ctx, client, apiPath); err != nil {
   151  			return fmt.Errorf("error deleting namespace %q: %v", apiPath, err)
   152  		}
   153  	}
   154  
   155  	// Do a final check to make sure that we got everything, and so that the
   156  	// caller doesn't assume that all namespaces are deleted when a glitch
   157  	// occurred due to namespaces changing while we were traversing or deleting
   158  	// them.
   159  	_, err = GetNamespaceIDPaths(client)
   160  	if err != nil && !errors.Is(err, ErrNotFound) {
   161  		return err
   162  	}
   163  
   164  	return nil
   165  }