Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some helpers to the sdk for working with namespaces. #25270

Merged
merged 6 commits into from
Feb 9, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions sdk/helper/testhelpers/namespaces/namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package namespaces

import (
"context"
"errors"
"fmt"
"path"
"slices"
"strings"
"time"

"github.com/hashicorp/vault/api"
)

// RootNamespacePath is the path of the root namespace.
const RootNamespacePath = ""

// RootNamespaceID is the ID of the root namespace.
const RootNamespaceID = "root"

// ErrNotFound is returned by funcs in this package when something isn't found,
// instead of returning (nil, nil).
var ErrNotFound = errors.New("not found")

// folderPath transforms an input path that refers to a namespace or mount point,
// such that it adheres to the norms Vault prefers. The result will have any
// leading "/" stripped, and, except for the root namespace which is always
// RootNamespacePath, will always end in a "/".
func folderPath(path string) string {
if !strings.HasSuffix(path, "/") {
path += "/"
}

return strings.TrimPrefix(path, "/")
}

// joinPath concatenates its inputs using "/" as a delimiter. The result will
// adhere to folderPath conventions.
func joinPath(s ...string) string {
return folderPath(path.Join(s...))
}

// GetNamespaceIDPaths does a namespace list and extracts the resulting paths
// and namespace IDs, returning a map from namespace ID to path. Returns
// ErrNotFound if no namespaces exist beneath the current namespace set on the
// client.
func GetNamespaceIDPaths(client *api.Client) (map[string]string, error) {
secret, err := client.Logical().List("sys/namespaces")
if err != nil {
return nil, err
}
if secret == nil {
return nil, ErrNotFound
}
if _, ok := secret.Data["key_info"]; !ok {
return nil, ErrNotFound
}

ret := map[string]string{}
for relNsPath, infoAny := range secret.Data["key_info"].(map[string]any) {
info := infoAny.(map[string]any)
id := info["id"].(string)
ret[id] = relNsPath
}
return ret, err
}

// WalkNamespaces does recursive namespace list commands to discover the complete
// namespace hierarchy. This may yield an error or inconsistent results if
// namespaces change while we're querying them.
// The callback f is invoked for every namespace discovered. Namespace traversal
// is pre-order depth-first. If f returns an error, traversal is aborted and the
// error is returned. Otherwise, an error is only returned if a request results
// in an error.
func WalkNamespaces(client *api.Client, f func(id, apiPath string) error) error {
return walkNamespacesRecursive(client, RootNamespaceID, RootNamespacePath, f)
}

func walkNamespacesRecursive(client *api.Client, startID, startApiPath string, f func(id, apiPath string) error) error {
if err := f(startID, startApiPath); err != nil {
return err
}

idpaths, err := GetNamespaceIDPaths(client.WithNamespace(startApiPath))
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil
}
return err
}

for id, path := range idpaths {
fullPath := joinPath(startApiPath, path)

if err = walkNamespacesRecursive(client, id, fullPath, f); err != nil {
return err
}
}
return nil
}

// PollDeleteNamespace issues a namespace delete request and waits for it
// to complete (since namespace deletes are asynchronous), at least until
// ctx expires.
func PollDeleteNamespace(ctx context.Context, client *api.Client, nsPath string) error {
_, err := client.Logical().Delete("sys/namespaces/" + nsPath)
if err != nil {
return err
}

LOOP:
for ctx.Err() == nil {
resp, err := client.Logical().Delete("sys/namespaces/" + nsPath)
if err != nil {
return err
}
for _, warn := range resp.Warnings {
if strings.HasPrefix(warn, "Namespace is already being deleted") {
time.Sleep(10 * time.Millisecond)
continue LOOP
}
}
break
}

return ctx.Err()
}

// DeleteAllNamespacesAndMounts uses WalkNamespaces to delete all namespaces,
// waiting for deletion to complete before returning. The same caveats about
// namespaces changing underneath us apply as in WalkNamespaces.
// Traversal is depth-first pre-order, but we must do the deletion in the reverse
// order, since a namespace containing namespaces cannot be deleted.
func DeleteAllNamespacesAndMounts(ctx context.Context, client *api.Client) error {
var nss []string
err := WalkNamespaces(client, func(id, apiPath string) error {
if apiPath != RootNamespacePath {
nss = append(nss, apiPath)
}
return nil
})
if err != nil {
return err
}
slices.Reverse(nss)
for _, apiPath := range nss {
if err := PollDeleteNamespace(ctx, client, apiPath); err != nil {
return fmt.Errorf("error deleting namespace %q: %v", apiPath, err)
}
}

// Do a final check to make sure that we got everything, and so that the
// caller doesn't assume that all namespaces are deleted when a glitch
// occurred due to namespaces changing while we were traversing or deleting
// them.
_, err = GetNamespaceIDPaths(client)
if err != nil && !errors.Is(err, ErrNotFound) {
return err
}

return nil
}
Loading