You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/testing/internal/e2e/tests/base/search_test.go

495 lines
16 KiB

// Copyright IBM Corp. 2020, 2025
// SPDX-License-Identifier: BUSL-1.1
package base_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/internal/clientcache"
"github.com/hashicorp/boundary/testing/internal/e2e"
"github.com/hashicorp/boundary/testing/internal/e2e/boundary"
"github.com/hashicorp/boundary/version"
"github.com/stretchr/testify/require"
)
// TestCliSearch asserts that the CLI can search for targets
func TestCliSearch(t *testing.T) {
e2e.MaybeSkipTest(t)
c, err := loadTestConfig()
require.NoError(t, err)
ctx := t.Context()
// If cache is already running, stop it so that we can start it with a
// shorter refresh interval
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
if output.Err == nil {
err := stopCacheAndWait(t, ctx)
require.NoError(t, err)
}
// Wait for cache to be up and running
statusResult, err := startCacheAndWait(t, ctx)
require.NoError(t, err)
require.GreaterOrEqual(t, statusResult.Item.Uptime, 0*time.Second)
require.Equal(t, 200, statusResult.StatusCode)
t.Cleanup(func() {
ctx := context.Background()
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "stop"))
require.NoError(t, err)
})
// Confirm cache version matches CLI version
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("version", "-format", "json"))
require.NoError(t, output.Err, string(output.Stderr))
var versionResult version.Info
err = json.Unmarshal(output.Stdout, &versionResult)
require.NoError(t, err)
require.Contains(t, statusResult.Item.Version, versionResult.Revision)
// Set up a new org and project
boundary.AuthenticateAdminCli(t, ctx)
orgId, err := boundary.CreateOrgCli(t, ctx)
require.NoError(t, err)
t.Cleanup(func() {
ctx := context.Background()
boundary.AuthenticateAdminCli(t, ctx)
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId))
require.NoError(t, output.Err, string(output.Stderr))
})
projectId, err := boundary.CreateProjectCli(t, ctx, orgId)
require.NoError(t, err)
// Get current number of targets
// Do a force-refresh first to ensure cache has the latest information
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-force-refresh", "true",
"-format", "json",
),
)
require.NoError(t, output.Err, string(output.Stderr))
var currentCount int
err = backoff.RetryNotify(
func() error {
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
if output.Err != nil {
return backoff.Permanent(errors.New(string(output.Stderr)))
}
statusResult = clientcache.StatusResult{}
err = json.Unmarshal(output.Stdout, &statusResult)
if err != nil {
return errors.New("Failed to unmarshal status result")
}
if len(statusResult.Item.Users) == 0 {
output = e2e.RunCommand(ctx, "cat", e2e.WithArgs(statusResult.Item.LogLocation))
t.Log("Printing cache log...")
t.Log(string(output.Stdout))
return errors.New("No users are appearing in the status")
}
idx := slices.IndexFunc(
statusResult.Item.Users[0].Resources,
func(r clientcache.ResourceStatus) bool {
return r.Name == "target"
},
)
if idx == -1 {
output = e2e.RunCommand(ctx, "cat", e2e.WithArgs(statusResult.Item.LogLocation))
t.Log("Printing cache log...")
t.Log(string(output.Stdout))
return errors.New("Targets not found in cache")
}
currentCount = statusResult.Item.Users[0].Resources[idx].Count
return nil
},
backoff.WithMaxRetries(backoff.NewConstantBackOff(3*time.Second), 5),
func(err error, td time.Duration) {
t.Logf("%s. Retrying...", err.Error())
},
)
require.NoError(t, err)
// Create enough targets to overflow a single page.
// Use the API to make creation faster.
t.Log("Creating targets...")
client, err := boundary.NewApiClient()
require.NoError(t, err)
tClient := targets.NewClient(client)
targetPort, err := strconv.ParseInt(c.TargetPort, 10, 32)
require.NoError(t, err)
var targetIds []string
targetPrefix := "test-target"
for i := 0; i < c.MaxPageSize+1; i++ {
resp, err := tClient.Create(ctx, "tcp", projectId,
targets.WithName(targetPrefix+strconv.Itoa(i)),
targets.WithTcpTargetDefaultPort(uint32(targetPort)),
targets.WithAddress(c.TargetAddress),
)
require.NoError(t, err)
targetIds = append(targetIds, resp.Item.Id)
}
// List targets.
// This requests data from the controller/database.
t.Log("Listing targets...")
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("targets", "list", "-scope-id", projectId, "-format", "json"))
require.NoError(t, output.Err, string(output.Stderr))
var targetListResult targets.TargetListResult
err = json.Unmarshal(output.Stdout, &targetListResult)
require.NoError(t, err)
var listedIds []string
for _, item := range targetListResult.Items {
listedIds = append(listedIds, item.Id)
}
require.Equal(t, len(targetIds), len(listedIds))
// Wait for data to be populated in the client cache
t.Log("Waiting for client cache to populate data...")
err = backoff.RetryNotify(
func() error {
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
if output.Err != nil {
return backoff.Permanent(errors.New(string(output.Stderr)))
}
var statusResult clientcache.StatusResult
err = json.Unmarshal(output.Stdout, &statusResult)
if err != nil {
return backoff.Permanent(err)
}
if len(statusResult.Item.Users) == 0 {
output = e2e.RunCommand(ctx, "cat", e2e.WithArgs(statusResult.Item.LogLocation))
t.Log("Printing cache log...")
t.Log(string(output.Stdout))
return errors.New("No users are appearing in the status")
}
idx := slices.IndexFunc(
statusResult.Item.Users[0].Resources,
func(r clientcache.ResourceStatus) bool {
return r.Name == "target"
},
)
if idx == -1 {
output = e2e.RunCommand(ctx, "cat", e2e.WithArgs(statusResult.Item.LogLocation))
t.Log("Printing cache log...")
t.Log(string(output.Stdout))
return errors.New("No targets are appearing in the status")
}
if statusResult.Item.Users[0].Resources[idx].Count != currentCount+c.MaxPageSize+1 {
return fmt.Errorf(
"Did not see expected number of targets in status, EXPECTED: %d, ACTUAL: %d",
currentCount+c.MaxPageSize+1,
statusResult.Item.Users[0].Resources[idx].Count,
)
}
t.Logf("Found %d target(s)", statusResult.Item.Users[0].Resources[idx].Count)
return nil
},
backoff.WithMaxRetries(backoff.NewConstantBackOff(3*time.Second), 5),
func(err error, td time.Duration) {
t.Logf("%s. Retrying...", err.Error())
},
)
require.NoError(t, err)
// Search for targets that contain the target prefix.
// This requests data from the client cache.
t.Log("Searching targets...")
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-format", "json",
"-query", fmt.Sprintf(`name %% "%s" and scope_id = "%s"`, targetPrefix, projectId),
),
)
require.NoError(t, output.Err, string(output.Stderr))
var searchResult clientcache.SearchResult
err = json.Unmarshal(output.Stdout, &searchResult)
var searchedIds []string
for _, target := range searchResult.Item.Targets {
searchedIds = append(searchedIds, target.Id)
}
require.Equal(t, len(targetIds), len(searchedIds))
require.Empty(t, cmp.Diff(listedIds, searchedIds, cmpopts.SortSlices(func(i, j string) bool { return i < j })))
// Do another search for a specific target name. Expect only 1 result
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-format", "json",
"-query", fmt.Sprintf(`name = "%s1" and scope_id = "%s"`, targetPrefix, projectId),
),
)
require.NoError(t, output.Err, string(output.Stderr))
searchResult = clientcache.SearchResult{}
err = json.Unmarshal(output.Stdout, &searchResult)
require.NoError(t, err)
require.Len(t, searchResult.Item.Targets, 1)
// Do another search for sessions with the force-refresh option set to true
// and no resources available.
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "sessions",
"-query", fmt.Sprintf(`scope_id = "%s"`, projectId),
"-force-refresh", "true",
"-format", "json",
),
)
require.NoError(t, output.Err, string(output.Stderr))
searchResult = clientcache.SearchResult{}
err = json.Unmarshal(output.Stdout, &searchResult)
require.NoError(t, err)
require.Len(t, searchResult.Item.Sessions, 0)
// Log out and confirm search does not work
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("logout"))
require.NoError(t, output.Err, string(output.Stderr))
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-format", "json",
"-query", fmt.Sprintf(`name %% "%s" and scope_id = "%s"`, targetPrefix, projectId),
),
)
require.Error(t, output.Err)
// Log back in and confirm search works
boundary.AuthenticateAdminCli(t, ctx)
err = backoff.RetryNotify(
func() error {
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-format", "json",
"-query", fmt.Sprintf(`name %% "%s" and scope_id = "%s"`, targetPrefix, projectId),
),
)
if output.Err != nil {
outputStatus := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
t.Log("Printing cache status...")
t.Log(string(outputStatus.Stdout))
var statusResult clientcache.StatusResult
err = json.Unmarshal(outputStatus.Stdout, &statusResult)
if err != nil {
return backoff.Permanent(err)
}
outputLog := e2e.RunCommand(ctx, "cat", e2e.WithArgs(statusResult.Item.LogLocation))
t.Log("Printing cache log...")
t.Log(string(outputLog.Stdout))
// BUG WORKAROUND: It seems like there's some weird interaction where
// occasionally, the cache fails to update after authentication
// on Linux environments
// https://hashicorp.atlassian.net/browse/ICU-16595
boundary.AuthenticateAdminCli(t, ctx)
return errors.New(string(output.Stderr))
}
searchResult := clientcache.SearchResult{}
err := json.Unmarshal(output.Stdout, &searchResult)
if err != nil {
return backoff.Permanent(err)
}
if len(searchResult.Item.Targets) != len(targetIds) {
return fmt.Errorf(
"Search did not return expected number of targets, EXPECTED: %d, ACTUAL: %d",
len(targetIds),
len(searchResult.Item.Targets),
)
}
return nil
},
backoff.WithMaxRetries(backoff.NewConstantBackOff(1*time.Second), 5),
func(err error, td time.Duration) {
t.Logf("%s. Retrying...", err.Error())
},
)
// Restart cache and confirm search works
err = stopCacheAndWait(t, ctx)
require.NoError(t, err)
statusResult, err = startCacheAndWait(t, ctx)
require.NoError(t, err)
require.GreaterOrEqual(t, statusResult.Item.Uptime, 0*time.Second)
require.Equal(t, 200, statusResult.StatusCode)
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-format", "json",
"-query", fmt.Sprintf(`name %% "%s" and scope_id = "%s"`, targetPrefix, projectId),
),
)
require.NoError(t, output.Err, string(output.Stderr))
searchResult = clientcache.SearchResult{}
err = json.Unmarshal(output.Stdout, &searchResult)
require.NoError(t, err)
require.Len(t, searchResult.Item.Targets, len(targetIds))
// Log out and restart cache. Log in and confirm search works
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("logout"))
require.NoError(t, output.Err, string(output.Stderr))
err = stopCacheAndWait(t, ctx)
require.NoError(t, err)
statusResult, err = startCacheAndWait(t, ctx)
require.NoError(t, err)
require.GreaterOrEqual(t, statusResult.Item.Uptime, 0*time.Second)
require.Equal(t, 200, statusResult.StatusCode)
boundary.AuthenticateAdminCli(t, ctx)
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"search",
"-resource", "targets",
"-format", "json",
"-query", fmt.Sprintf(`name %% "%s" and scope_id = "%s"`, targetPrefix, projectId),
),
)
require.NoError(t, output.Err, string(output.Stderr))
searchResult = clientcache.SearchResult{}
err = json.Unmarshal(output.Stdout, &searchResult)
require.NoError(t, err)
require.Len(t, searchResult.Item.Targets, len(targetIds))
}
func startCacheAndWait(t *testing.T, ctx context.Context) (clientcache.StatusResult, error) {
t.Helper()
t.Log("Starting cache...")
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"cache", "start",
"-refresh-interval", "5s",
"-background",
),
)
if output.Err != nil {
return clientcache.StatusResult{}, errors.New(string(output.Stderr))
}
var statusResult clientcache.StatusResult
err := backoff.RetryNotify(
func() error {
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
if output.Err != nil {
return errors.New(strings.TrimSpace(string(output.Stderr)))
}
statusResult = clientcache.StatusResult{}
err := json.Unmarshal(output.Stdout, &statusResult)
if err != nil {
return backoff.Permanent(err)
}
return nil
},
backoff.WithMaxRetries(backoff.NewConstantBackOff(1*time.Second), 5),
func(err error, td time.Duration) {
t.Logf("%s. Retrying...", err.Error())
},
)
return statusResult, err
}
func stopCacheAndWait(t *testing.T, ctx context.Context) error {
t.Helper()
// Get latest status if cache is already running.
var statusResult clientcache.StatusResult
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
if output.Err == nil {
err := json.Unmarshal(output.Stdout, &statusResult)
if err != nil {
return err
}
}
t.Log("Stopping cache...")
output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "stop"))
if output.Err != nil {
return errors.New(string(output.Stderr))
}
err := backoff.RetryNotify(
func() error {
output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("cache", "status", "-format", "json"))
// When the cache is not running, `boundary cache status` exits non-zero
// and prints "The cache process is not running." to stderr. Treat that
// condition as success, even if output.Err is non-nil.
if strings.Contains(string(output.Stderr), "The cache process is not running.") {
return nil
}
// For other non-zero exits, surface the error and retry.
if output.Err != nil {
return errors.New(strings.TrimSpace(string(output.Stderr)))
}
// Cache is still running; keep retrying until it stops.
return fmt.Errorf("waiting for cache to stop: %s", strings.TrimSpace(string(output.Stdout)))
},
backoff.WithMaxRetries(backoff.NewConstantBackOff(1*time.Second), 5),
func(err error, td time.Duration) {
t.Logf("%s. Retrying...", err.Error())
},
)
if err != nil {
return err
}
// check domain socket
if statusResult.Item.SocketAddress != "" {
t.Log("Checking if cache socket is cleaned up...")
err = backoff.RetryNotify(
func() error {
_, statErr := os.Stat(statusResult.Item.SocketAddress)
if statErr == nil {
return errors.New("cache socket still exists")
}
if os.IsNotExist(statErr) {
return nil
}
return backoff.Permanent(fmt.Errorf("failed to stat cache socket: %w", statErr))
},
backoff.WithMaxRetries(backoff.NewConstantBackOff(1*time.Second), 5),
func(err error, td time.Duration) {
t.Logf("%s. Retrying...", err.Error())
},
)
}
return err
}