test(e2e): add GCP dynamic host catalog e2e tests (#5294)

* test(e2e): add GCP infra resources and tests for enos testing

- add 1 new `gcp_target` module that creates a GCP VM instance with labels for filtering
- add GCP dynamic host catalog e2e tests
This commit is contained in:
Elim Tsiagbey authored and GitHub committed 2024-12-17 14:06:14 -05:00
1 parent 862a30c389
commit a4f2a704fd
16 files changed
+992 -6

No files matched your search

+52 -2
View File
@@ -240,9 +240,9 @@ func CreateAwsHostCatalogCli(t testing.TB, ctx context.Context, projectId, acces
return hostCatalogId, nil
}
// CreateAwsHostSetCli uses the cli to create a new host set from an AWS dynamic host catalog.
// CreatePluginHostSetCli uses the cli to create a new host set from a dynamic host catalog.
// Returns the id of the new host set.
func CreateAwsHostSetCli(t testing.TB, ctx context.Context, hostCatalogId string, filter string) (string, error) {
func CreatePluginHostSetCli(t testing.TB, ctx context.Context, hostCatalogId string, filter string) (string, error) {
name, err := base62.Random(16)
if err != nil {
return "", err
@@ -359,3 +359,53 @@ func WaitForNumberOfHostsInHostSetCli(t testing.TB, ctx context.Context, hostSet
)
require.NoError(t, err)
}
// CreateGcpHostCatalogCli uses the cli to create a new GCP dynamic host catalog.
// Returns the id of the new host catalog.
func CreateGcpHostCatalogCli(
t testing.TB,
ctx context.Context,
projectId string,
gcpProjectId string,
clientEmail string,
privateKeyId string,
privateKey string,
zone string,
) (string, error) {
name, err := base62.Random(16)
if err != nil {
return "", err
}
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"host-catalogs", "create", "plugin",
"-scope-id", projectId,
"-plugin-name", "gcp",
"-attr", "disable_credential_rotation=true",
"-attr", fmt.Sprintf("project_id=%s", gcpProjectId),
"-attr", fmt.Sprintf("client_email=%s", clientEmail),
"-attr", fmt.Sprintf("zone=%s", zone),
"-secret", "private_key_id=env://E2E_GCP_PRIVATE_KEY_ID",
"-secret", "private_key=env://E2E_GCP_PRIVATE_KEY",
"-name", fmt.Sprintf("e2e Host Catalog %s", name),
"-description", "e2e",
"-format", "json",
),
e2e.WithEnv("E2E_GCP_PRIVATE_KEY_ID", privateKeyId),
e2e.WithEnv("E2E_GCP_PRIVATE_KEY", privateKey),
)
if output.Err != nil {
return "", fmt.Errorf("%w: %s", output.Err, string(output.Stderr))
}
var createHostCatalogResult hostcatalogs.HostCatalogCreateResult
err = json.Unmarshal(output.Stdout, &createHostCatalogResult)
if err != nil {
return "", err
}
hostCatalogId := createHostCatalogResult.Item.Id
t.Logf("Created Host Catalog: %s", hostCatalogId)
return hostCatalogId, nil
}
@@ -42,7 +42,7 @@ func TestCliCreateAwsDynamicHostCatalogWithEmptyHostSet(t *testing.T) {
require.NoError(t, err)
// Set up a host set
hostSetId, err := boundary.CreateAwsHostSetCli(t, ctx, hostCatalogId, "tag:empty_test=true")
hostSetId, err := boundary.CreatePluginHostSetCli(t, ctx, hostCatalogId, "tag:empty_test=true")
require.NoError(t, err)
// Check that there are no hosts in the host set
@@ -47,7 +47,7 @@ func TestCliCreateAwsDynamicHostCatalogWithHostSet(t *testing.T) {
require.NoError(t, err)
// Set up a host set
hostSetId1, err := boundary.CreateAwsHostSetCli(t, ctx, hostCatalogId, c.AwsHostSetFilter1)
hostSetId1, err := boundary.CreatePluginHostSetCli(t, ctx, hostCatalogId, c.AwsHostSetFilter1)
require.NoError(t, err)
var targetIps1 []string
err = json.Unmarshal([]byte(c.AwsHostSetIps1), &targetIps1)
@@ -56,7 +56,7 @@ func TestCliCreateAwsDynamicHostCatalogWithHostSet(t *testing.T) {
boundary.WaitForNumberOfHostsInHostSetCli(t, ctx, hostSetId1, expectedHostSetCount1)
// Set up another host set
hostSetId2, err := boundary.CreateAwsHostSetCli(t, ctx, hostCatalogId, c.AwsHostSetFilter2)
hostSetId2, err := boundary.CreatePluginHostSetCli(t, ctx, hostCatalogId, c.AwsHostSetFilter2)
require.NoError(t, err)
var targetIps2 []string
err = json.Unmarshal([]byte(c.AwsHostSetIps2), &targetIps2)
@@ -243,7 +243,7 @@ func populateBoundaryDatabase(t testing.TB, ctx context.Context, c *config, te T
// Create AWS dynamic host catalog
awsHostCatalogId, err := boundary.CreateAwsHostCatalogCli(t, ctx, projectId, c.AwsAccessKeyId, c.AwsSecretAccessKey, c.AwsRegion)
require.NoError(t, err)
awsHostSetId, err := boundary.CreateAwsHostSetCli(t, ctx, awsHostCatalogId, c.AwsHostSetFilter)
awsHostSetId, err := boundary.CreatePluginHostSetCli(t, ctx, awsHostCatalogId, c.AwsHostSetFilter)
require.NoError(t, err)
boundary.WaitForHostsInHostSetCli(t, ctx, awsHostSetId)
@@ -0,0 +1,138 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package gcp_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"testing"
"time"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/testing/internal/e2e"
"github.com/hashicorp/boundary/testing/internal/e2e/boundary"
"github.com/stretchr/testify/require"
)
// TestCliCreateGcpDynamicHostCatalogWithEmptyHostSet uses the boundary cli to create a host catalog with the GCP
// plugin. The test sets up an GCP dynamic host catalog, creates some host sets, sets up a target to
// one of the host sets, and attempts to connect to the target.
func TestCliCreateGcpDynamicHostCatalogWithEmptyHostSet(t *testing.T) {
e2e.MaybeSkipTest(t)
c, err := loadTestConfig()
require.NoError(t, err)
ctx := context.Background()
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)
hostCatalogId, err := boundary.CreateGcpHostCatalogCli(t, ctx, projectId, c.GcpProjectId, c.GcpClientEmail, c.GcpPrivateKeyId, c.GcpPrivateKey, c.GcpZone)
require.NoError(t, err)
// Set up a host set
hostSetId, err := boundary.CreatePluginHostSetCli(t, ctx, hostCatalogId, "labels.empty_test=true")
require.NoError(t, err)
// Check that there are no hosts in the host set
t.Logf("Looking for items in the host set...")
var actualHostSetCount int
for i := 0; i < 3; i++ {
if i != 0 {
time.Sleep(3 * time.Second)
}
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"host-sets", "read",
"-id", hostSetId,
"-format", "json",
),
)
require.NoError(t, output.Err, string(output.Stderr))
var hostSetsReadResult hostsets.HostSetReadResult
err := json.Unmarshal(output.Stdout, &hostSetsReadResult)
require.NoError(t, err)
actualHostSetCount = len(hostSetsReadResult.Item.HostIds)
require.Equal(t, 0, actualHostSetCount,
fmt.Sprintf("Detected incorrect number of hosts. Expected: 0, Actual: %d", actualHostSetCount),
)
}
t.Log("Successfully detected zero hosts in the host set")
// Check that there are no hosts in the host catalog
t.Logf("Looking for items in the host catalog...")
var actualHostCatalogCount int
for i := 0; i < 3; i++ {
if i != 0 {
time.Sleep(3 * time.Second)
}
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs("hosts", "list", "-host-catalog-id", hostCatalogId, "-format", "json"),
)
require.NoError(t, output.Err, string(output.Stderr))
var hostCatalogListResult hostcatalogs.HostCatalogListResult
err := json.Unmarshal(output.Stdout, &hostCatalogListResult)
require.NoError(t, err)
actualHostCatalogCount = len(hostCatalogListResult.Items)
require.Equal(t, 0, actualHostCatalogCount,
fmt.Sprintf("Detected incorrect number of hosts. Expected: 0, Actual: %d", actualHostCatalogCount),
)
}
t.Log("Successfully detected zero hosts in the host catalog")
// Create target
targetId, err := boundary.CreateTargetCli(t, ctx, projectId, c.GcpTargetPort)
require.NoError(t, err)
err = boundary.AddHostSourceToTargetCli(t, ctx, targetId, hostSetId)
require.NoError(t, err)
// Create a temporary file to store the SSH key string
tempFile, err := os.CreateTemp("./", "ssh-key.pem")
require.NoError(t, err)
defer os.Remove(tempFile.Name())
// Write the SSH key string to the temporary file
_, err = tempFile.WriteString(c.GcpTargetSshKey)
require.NoError(t, err)
err = tempFile.Close()
require.NoError(t, err)
// Attempt to connect to target
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"connect",
"-target-id", targetId,
"-format", "json",
"-exec", "/usr/bin/ssh", "--",
"-l", c.GcpTargetSshUser,
"-i", tempFile.Name(),
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "IdentitiesOnly=yes", // forces the use of the provided key
"-p", "{{boundary.port}}", // this is provided by boundary
"{{boundary.ip}}",
"hostname", "-i",
),
)
var response boundary.CliError
err = json.Unmarshal(output.Stderr, &response)
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, response.Status, "Expected to error when connecting to a target with zero hosts")
t.Log("Successfully failed to connect to target")
}
@@ -0,0 +1,281 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package gcp_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/boundary/api/hosts"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/testing/internal/e2e"
"github.com/hashicorp/boundary/testing/internal/e2e/boundary"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestCliCreateGcpDynamicHostCatalogWithHostSet uses the boundary cli to create a host catalog with the GCP
// plugin. The test sets up an GCP dynamic host catalog, creates some host sets, sets up a target to
// one of the host sets, and attempts to connect to the target.
func TestCliCreateGcpDynamicHostCatalogWithHostSet(t *testing.T) {
e2e.MaybeSkipTest(t)
c, err := loadTestConfig()
require.NoError(t, err)
ctx := context.Background()
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)
hostCatalogId, err := boundary.CreateGcpHostCatalogCli(t, ctx, projectId, c.GcpProjectId, c.GcpClientEmail, c.GcpPrivateKeyId, c.GcpPrivateKey, c.GcpZone)
require.NoError(t, err)
// Set up a host set
hostSetId1, err := boundary.CreatePluginHostSetCli(t, ctx, hostCatalogId, c.GcpHostSetFilter1)
require.NoError(t, err)
boundary.WaitForNumberOfHostsInHostSetCli(t, ctx, hostSetId1, 1)
// Set up another host set
hostSetId2, err := boundary.CreatePluginHostSetCli(t, ctx, hostCatalogId, c.GcpHostSetFilter2)
require.NoError(t, err)
boundary.WaitForNumberOfHostsInHostSetCli(t, ctx, hostSetId2, 1)
// Update host set with a different filter
t.Log("Updating host set 2 with host set 1's filter...")
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"host-sets", "update", "plugin",
"-id", hostSetId2,
"-attr", fmt.Sprintf("filters=%s", c.GcpHostSetFilter1),
),
)
require.NoError(t, output.Err, string(output.Stderr))
boundary.WaitForNumberOfHostsInHostSetCli(t, ctx, hostSetId2, 1)
// update host set to use preferred endpoints
t.Log("Updating host set 1 to use preferred endpoint...")
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"host-sets", "update", "plugin",
"-id", hostSetId1,
"-preferred-endpoint", fmt.Sprintf("cidr:%s/32", c.GcpTargetAddress),
),
)
require.NoError(t, output.Err, string(output.Stderr))
// Get list of all hosts from host catalog
t.Logf("Looking for items in the host catalog...")
var actualHostCatalogCount int
err = backoff.RetryNotify(
func() error {
output := e2e.RunCommand(ctx, "boundary",
e2e.WithArgs("hosts", "list", "-host-catalog-id", hostCatalogId, "-format", "json"),
)
if output.Err != nil {
return backoff.Permanent(errors.New(string(output.Stderr)))
}
var hostCatalogListResult hostcatalogs.HostCatalogListResult
err := json.Unmarshal(output.Stdout, &hostCatalogListResult)
if err != nil {
return backoff.Permanent(err)
}
t.Logf("Found %v host(s)", len(hostCatalogListResult.GetItems()))
actualHostCatalogCount = len(hostCatalogListResult.Items)
if actualHostCatalogCount == 0 {
return errors.New("No items are appearing in the host catalog")
}
t.Logf("Found %d host(s)", actualHostCatalogCount)
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)
assert.Equal(t, 1, actualHostCatalogCount, "Numbers of hosts in host catalog did not match expected amount")
// Create target
targetId, err := boundary.CreateTargetCli(t, ctx, projectId, c.GcpTargetPort)
require.NoError(t, err)
err = boundary.AddHostSourceToTargetCli(t, ctx, targetId, hostSetId1)
require.NoError(t, err)
// Create a temporary file to store the SSH key string
tempFile, err := os.CreateTemp("./", "ssh-key.pem")
require.NoError(t, err)
defer os.Remove(tempFile.Name())
// Write the SSH key string to the temporary file
_, err = tempFile.WriteString(c.GcpTargetSshKey)
require.NoError(t, err)
err = tempFile.Close()
require.NoError(t, err)
// Connect to target
output = e2e.RunCommand(ctx, "boundary",
e2e.WithArgs(
"connect",
"-target-id", targetId,
"-exec", "/usr/bin/ssh", "--",
"-l", c.GcpTargetSshUser,
"-i", tempFile.Name(),
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
"-o", "IdentitiesOnly=yes", // forces the use of the provided key
"-p", "{{boundary.port}}", // this is provided by boundary
"{{boundary.ip}}",
"hostname", "-i",
),
)
require.NoError(t, output.Err, string(output.Stderr))
parts := strings.Fields(string(output.Stdout))
hostIp := parts[len(parts)-1]
t.Log("Successfully connected to the target")
// Check if connected host exists in the host set
var targetIps []string
err = json.Unmarshal([]byte(c.GcpHostSetIps), &targetIps)
require.NoError(t, err)
hostIpInList := false
for _, v := range targetIps {
if v == hostIp {
hostIpInList = true
}
}
require.True(t, hostIpInList, fmt.Sprintf("Connected host (%s) is not in expected list (%s)", hostIp, targetIps))
}
// TestApiCreateGcpDynamicHostCatalog uses the Go api to create a host catalog with the GCP plugin.
// The test sets up an GCP dynamic host catalog, creates a host set, and sets up a target to the
// host set.
func TestApiCreateGCPDynamicHostCatalog(t *testing.T) {
e2e.MaybeSkipTest(t)
c, err := loadTestConfig()
require.NoError(t, err)
client, err := boundary.NewApiClient()
require.NoError(t, err)
ctx := context.Background()
orgId, err := boundary.CreateOrgApi(t, ctx, client)
require.NoError(t, err)
t.Cleanup(func() {
scopeClient := scopes.NewClient(client)
_, err := scopeClient.Delete(ctx, orgId)
require.NoError(t, err)
})
projectId, err := boundary.CreateProjectApi(t, ctx, client, orgId)
require.NoError(t, err)
// Create a dynamic host catalog
hcClient := hostcatalogs.NewClient(client)
newHostCatalogResult, err := hcClient.Create(ctx, "plugin", projectId,
hostcatalogs.WithName("e2e Automated Test Host Catalog"),
hostcatalogs.WithPluginName("gcp"),
hostcatalogs.WithAttributes(map[string]any{
"disable_credential_rotation": true,
"project_id": c.GcpProjectId,
"client_email": c.GcpClientEmail,
"zone": c.GcpZone,
}),
hostcatalogs.WithSecrets(map[string]any{
"private_key_id": c.GcpPrivateKeyId,
"private_key": c.GcpPrivateKey,
}),
)
require.NoError(t, err)
newHostCatalogId := newHostCatalogResult.Item.Id
t.Logf("Created Host Catalog: %s", newHostCatalogId)
// Create a host set and add to catalog
hsClient := hostsets.NewClient(client)
newHostSetResult, err := hsClient.Create(ctx, newHostCatalogId,
hostsets.WithAttributes(map[string]any{
"filters": c.GcpHostSetFilter1,
}),
hostsets.WithName("e2e Automated Test Host Set"),
)
require.NoError(t, err)
newHostSetId := newHostSetResult.Item.Id
t.Logf("Created Host Set: %s", newHostSetId)
// Get list of hosts in host set
// Retry is needed here since it can take a few tries before hosts start appearing
t.Logf("Looking for items in the host set...")
var actualHostSetCount int
err = backoff.RetryNotify(
func() error {
hostSetReadResult, err := hsClient.Read(ctx, newHostSetId)
if err != nil {
return backoff.Permanent(err)
}
actualHostSetCount = len(hostSetReadResult.Item.HostIds)
if actualHostSetCount == 0 {
return errors.New("No items are appearing in the host set")
}
t.Logf("Found %d hosts", actualHostSetCount)
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)
t.Log("Successfully found items in the host set")
assert.Equal(t, 1, actualHostSetCount, "Numbers of hosts in host set did not match expected amount")
// Get list of all hosts from host catalog
// Retry is needed here since it can take a few tries before hosts start appearing
t.Logf("Looking for items in the host catalog...")
var actualHostCatalogCount int
hClient := hosts.NewClient(client)
err = backoff.RetryNotify(
func() error {
hostListResult, err := hClient.List(ctx, newHostCatalogId)
if err != nil {
return backoff.Permanent(err)
}
actualHostCatalogCount = len(hostListResult.Items)
if actualHostCatalogCount == 0 {
return errors.New("No items are appearing in the host catalog")
}
t.Logf("Found %d hosts", actualHostCatalogCount)
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)
t.Log("Successfully found items in the host catalog")
assert.Equal(t, 1, actualHostCatalogCount, "Numbers of hosts in host catalog did not match expected amount")
}
@@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package gcp_test
import "github.com/kelseyhightower/envconfig"
type config struct {
GcpPrivateKeyId string `envconfig:"E2E_GCP_PRIVATE_KEY_ID" required:"true"`
GcpPrivateKey string `envconfig:"E2E_GCP_PRIVATE_KEY" required:"true"`
GcpZone string `envconfig:"E2E_GCP_ZONE" required:"true"` // e.g. "us-central1-a"
GcpProjectId string `envconfig:"E2E_GCP_PROJECT_ID" required:"true"` // e.g. "my-project"
GcpClientEmail string `envconfig:"E2E_GCP_CLIENT_EMAIL" required:"true"`
GcpHostSetFilter1 string `envconfig:"E2E_GCP_HOST_SET_FILTER1" required:"true"`
GcpHostSetFilter2 string `envconfig:"E2E_GCP_HOST_SET_FILTER2" required:"true"`
GcpHostSetIps string `envconfig:"E2E_GCP_HOST_SET_IPS" required:"true"`
GcpTargetSshKey string `envconfig:"E2E_GCP_TARGET_SSH_KEY" required:"true"`
GcpTargetAddress string `envconfig:"E2E_TARGET_ADDRESS" required:"true"` // e.g. "192.168.0.1"
GcpTargetSshUser string `envconfig:"E2E_SSH_USER" required:"true"` // e.g. "ubuntu"
GcpTargetPort string `envconfig:"E2E_TARGET_PORT" required:"true"` // e.g. "22"
}
func loadTestConfig() (*config, error) {
var c config
err := envconfig.Process("", &c)
if err != nil {
return nil, err
}
return &c, nil
}