From 636ea5ada08d39819739e399f66afeebf64327b3 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Wed, 7 Feb 2024 15:01:20 -0500 Subject: [PATCH 1/5] Add some helpers to the sdk for working with namespaces. --- .../testhelpers/namespaces/namespaces.go | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 sdk/helper/testhelpers/namespaces/namespaces.go diff --git a/sdk/helper/testhelpers/namespaces/namespaces.go b/sdk/helper/testhelpers/namespaces/namespaces.go new file mode 100644 index 000000000000..101447878dd2 --- /dev/null +++ b/sdk/helper/testhelpers/namespaces/namespaces.go @@ -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 +} From bb5694c59afd78b60da8bcfc78971dacf6af480a Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Fri, 9 Feb 2024 08:09:05 -0500 Subject: [PATCH 2/5] Add copyright header. --- sdk/helper/testhelpers/namespaces/namespaces.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/helper/testhelpers/namespaces/namespaces.go b/sdk/helper/testhelpers/namespaces/namespaces.go index 101447878dd2..d3dca265c487 100644 --- a/sdk/helper/testhelpers/namespaces/namespaces.go +++ b/sdk/helper/testhelpers/namespaces/namespaces.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package namespaces import ( @@ -20,7 +23,7 @@ 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") +var ErrNotFound = errors.New("no namespaces 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 From cc20d90a69dbd8a7329899e32e2b40ea1a57c3d7 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Fri, 9 Feb 2024 08:12:23 -0500 Subject: [PATCH 3/5] Add CL --- changelog/25270.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/25270.txt diff --git a/changelog/25270.txt b/changelog/25270.txt new file mode 100644 index 000000000000..60ccf37dc69f --- /dev/null +++ b/changelog/25270.txt @@ -0,0 +1,3 @@ +```release-note:improvement +sdk/helper/testhelpers: add namespace helpers +``` From f651957fdd45ba6ec457ab26d79418e6bbb5a1b9 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Fri, 9 Feb 2024 08:20:08 -0500 Subject: [PATCH 4/5] Correct copyright --- sdk/helper/testhelpers/namespaces/namespaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/helper/testhelpers/namespaces/namespaces.go b/sdk/helper/testhelpers/namespaces/namespaces.go index d3dca265c487..4cb9e518f38c 100644 --- a/sdk/helper/testhelpers/namespaces/namespaces.go +++ b/sdk/helper/testhelpers/namespaces/namespaces.go @@ -1,5 +1,5 @@ // Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MPL-2.0 package namespaces From 020ef18d087c13b4bf290c5226cfff9aa54c1f98 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Fri, 9 Feb 2024 08:58:51 -0500 Subject: [PATCH 5/5] Name was misleading: root mounts won't be deleted --- sdk/helper/testhelpers/namespaces/namespaces.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/helper/testhelpers/namespaces/namespaces.go b/sdk/helper/testhelpers/namespaces/namespaces.go index 4cb9e518f38c..9981104f2b56 100644 --- a/sdk/helper/testhelpers/namespaces/namespaces.go +++ b/sdk/helper/testhelpers/namespaces/namespaces.go @@ -129,12 +129,12 @@ LOOP: return ctx.Err() } -// DeleteAllNamespacesAndMounts uses WalkNamespaces to delete all namespaces, +// DeleteAllNamespaces 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 { +func DeleteAllNamespaces(ctx context.Context, client *api.Client) error { var nss []string err := WalkNamespaces(client, func(id, apiPath string) error { if apiPath != RootNamespacePath {