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 }