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

+16
View File
@@ -80,6 +80,7 @@ jobs:
- filter: 'e2e_database'
- filter: 'e2e_docker_base builder:crt'
- filter: 'e2e_docker_base_plus builder:crt'
- filter: 'e2e_docker_base_with_gcp builder:crt'
- filter: 'e2e_docker_base_with_vault builder:crt'
- filter: 'e2e_docker_base_with_worker builder:crt'
- filter: 'e2e_docker_worker_registration_controller_led builder:crt'
@@ -101,6 +102,10 @@ jobs:
ENOS_VAR_boundary_docker_image_name: ${{ inputs.docker-image-name }}
ENOS_VAR_boundary_docker_image_file: ./support/boundary_docker_image.tar
ENOS_VAR_go_version: ${{ inputs.go-version }}
ENOS_VAR_gcp_project_id: ${{ secrets.GCP_PROJECT_ID_CI }}
ENOS_VAR_gcp_client_email: ${{ secrets.GCP_CLIENT_EMAIL_CI }}
ENOS_VAR_gcp_private_key_id: ${{ secrets.GCP_PRIVATE_KEY_ID_CI }}
ENOS_VAR_gcp_private_key: ${{ secrets.GCP_PRIVATE_KEY_CI }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -147,6 +152,17 @@ jobs:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_CI }}
role-skip-session-tagging: true
role-duration-seconds: 3600
- name: Configure GCP credentials
if: contains(matrix.filter, 'gcp')
id: gcp_auth
uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
access_token_lifetime: '3600s'
project_id: ${{ secrets.GCP_PROJECT_ID_CI }}
- name: 'Set up GCP Cloud SDK'
if: contains(matrix.filter, 'gcp')
uses: google-github-actions/setup-gcloud@98ddc00a17442e89a24bbf282954a3b65ce6d200 # v2.1.0
- name: Set up Enos
uses: hashicorp/action-setup-enos@v1 # TSCCR: loading action configs: failed to query HEAD reference: failed to get advertised references: authorization failed
with:
+12
View File
@@ -178,3 +178,15 @@ module "docker_ldap" {
module "docker_minio" {
source = "./modules/docker_minio"
}
module "gcp_iam_setup" {
source = "./modules/gcp_iam_setup"
gcp_project_id = var.gcp_project_id
}
module "gcp_target" {
source = "./modules/gcp_target"
target_count = var.target_count
environment = var.environment
enos_user = var.enos_user
}
@@ -0,0 +1,143 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
# For this scenario to work, add the following line to /etc/hosts
# 127.0.0.1 localhost boundary
scenario "e2e_docker_base_with_gcp" {
terraform_cli = terraform_cli.default
terraform = terraform.default
providers = [
provider.enos.default,
provider.google.default
]
matrix {
builder = ["local", "crt"]
}
locals {
local_boundary_dir = var.local_boundary_dir != null ? abspath(var.local_boundary_dir) : null
local_boundary_src_dir = var.local_boundary_src_dir != null ? abspath(var.local_boundary_src_dir) : null
boundary_docker_image_file = abspath(var.boundary_docker_image_file)
license_path = abspath(var.boundary_license_path != null ? var.boundary_license_path : joinpath(path.root, "./support/boundary.hclic"))
gcp_private_key = var.gcp_private_key_path != null ? file(var.gcp_private_key_path) : var.gcp_private_key
network_cluster = "e2e_gcp"
build_path = {
"local" = "/tmp",
"crt" = var.crt_bundle_path == null ? null : abspath(var.crt_bundle_path)
}
tags = merge({
"Project Name" : var.project_name
"Project" : "Enos",
"Environment" : "ci"
}, var.tags)
}
step "build_boundary_docker_image" {
module = matrix.builder == "crt" ? module.build_boundary_docker_crt : module.build_boundary_docker_local
variables {
path = matrix.builder == "crt" ? local.boundary_docker_image_file : ""
cli_build_path = local.build_path[matrix.builder]
edition = var.boundary_edition
}
}
step "create_docker_network" {
module = module.docker_network
variables {
network_name = local.network_cluster
}
}
step "create_boundary_database" {
depends_on = [
step.create_docker_network
]
variables {
image_name = "${var.docker_mirror}/library/postgres:latest"
network_name = [local.network_cluster]
}
module = module.docker_postgres
}
step "read_license" {
skip_step = var.boundary_edition == "oss"
module = module.read_license
variables {
file_name = local.license_path
}
}
step "create_boundary" {
module = module.docker_boundary
depends_on = [
step.create_docker_network,
step.create_boundary_database,
step.build_boundary_docker_image
]
variables {
image_name = matrix.builder == "crt" ? var.boundary_docker_image_name : step.build_boundary_docker_image.image_name
network_name = [local.network_cluster]
database_network = local.network_cluster
postgres_address = step.create_boundary_database.address
boundary_license = var.boundary_edition != "oss" ? step.read_license.license : ""
}
}
step "create_test_id" {
module = module.random_stringifier
variables {
length = 5
}
}
step "create_gcp_target" {
module = module.gcp_target
variables {
enos_user = var.enos_user
instance_type = var.gcp_target_instance_type
gcp_zone = var.gcp_zone
target_count = 1
}
}
step "run_e2e_test" {
module = module.test_e2e_docker
depends_on = [
step.create_boundary,
step.create_gcp_target
]
variables {
test_package = "github.com/hashicorp/boundary/testing/internal/e2e/tests/gcp"
docker_mirror = var.docker_mirror
network_name = step.create_docker_network.network_name
go_version = var.go_version
debug_no_run = var.e2e_debug_no_run
alb_boundary_api_addr = step.create_boundary.address
auth_method_id = step.create_boundary.auth_method_id
auth_login_name = step.create_boundary.login_name
auth_password = step.create_boundary.password
local_boundary_dir = step.build_boundary_docker_image.cli_zip_path
local_boundary_src_dir = local.local_boundary_src_dir
gcp_host_set_filter1 = step.create_gcp_target.filter_label1
gcp_host_set_filter2 = step.create_gcp_target.filter_label2
gcp_private_key_id = var.gcp_private_key_id
gcp_private_key = local.gcp_private_key
gcp_zone = var.gcp_zone
gcp_project_id = var.gcp_project_id
gcp_client_email = var.gcp_client_email
gcp_target_ssh_key = step.create_gcp_target.target_ssh_key
gcp_host_set_ips = step.create_gcp_target.target_ips
target_address = step.create_gcp_target.target_public_ips[0]
target_port = "22"
target_user = "ubuntu"
max_page_size = step.create_boundary.max_page_size
}
}
}
+53
View File
@@ -200,3 +200,56 @@ variable "hcp_boundary_cluster_id" {
// If using HCP int, ensure that the cluster id starts with "int-"
// Example: "int-19283a-123123-..."
}
variable "gcp_target_instance_type" {
description = "Instance type for test target nodes"
type = string
default = "e2-micro"
}
variable "gcp_region" {
description = "GCP region where the resources will be created"
type = string
default = "us-central1"
}
variable "gcp_zone" {
description = "GCP zone where the resources will be created"
type = string
default = "us-central1-a"
}
variable "gcp_project_id" {
description = "GCP project where the resources will be created"
type = string
sensitive = true
default = ""
}
variable "gcp_private_key_path" {
description = "Path to the GCP private key"
type = string
sensitive = true
default = null
}
variable "gcp_private_key" {
description = "GCP private key"
type = string
sensitive = true
default = null
}
variable "gcp_private_key_id" {
description = "GCP private key ID"
type = string
sensitive = true
default = null
}
variable "gcp_client_email" {
description = "GCP client email"
type = string
sensitive = true
default = null
}
+10
View File
@@ -17,6 +17,11 @@ terraform "default" {
source = "hashicorp/aws"
version = "5.72.1"
}
google = {
source = "hashicorp/google"
version = "5.22.0"
}
}
}
@@ -32,3 +37,8 @@ provider "enos" "default" {
}
}
}
provider "google" "default" {
region = var.gcp_region
project = var.gcp_project_id
}
+17
View File
@@ -60,6 +60,23 @@
// Number of target instances to create. Applies to AWS scenarios only.
// target_count = 1
// The GCP project ID to use for the tests. Only needed if running GCP scenarios.
// gcp_project_id = "my-gcp-project-id"
// The GCP private_key_path. This is used to authenticate with GCP. Only needed
// if running GCP scenarios. This should not be used in combination with gcp_private_key.
// gcp_private_key_path = ""
// The GCP private_key. This is used to authenticate with GCP. Only needed
// if running GCP scenarios. This should not be used in combination with gcp_private_key_path.
// gcp_private_key = ""
// The GCP private_key_id. Only needed if running GCP scenarios.
// gcp_private_key_id = ""
// The GCP client_email used to authenticate with GCP
// gcp_client_email = "my-gcp-client-email"
// The directory that contains the copy of the boundary cli that the e2e tests
// will use in CI. Only needed if e2e_debug_no_run = false.
// local_boundary_dir = "/Users/<user>/.go/bin"
+160
View File
@@ -0,0 +1,160 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
terraform {
required_providers {
enos = {
source = "registry.terraform.io/hashicorp-forge/enos"
}
}
}
variable "target_count" {}
variable "enos_user" {}
variable "additional_labels" {
default = {}
}
variable "instance_type" {
description = "The type of instance to create."
type = string
default = "e2-micro"
}
variable "environment" {
description = "Name of the environment."
type = string
default = "enos-environment"
}
variable "private_cidr_block" {
type = list(string)
default = ["10.0.0.0/8"]
}
variable "gcp_zone" {
description = "The zone to deploy the resources."
type = string
default = "us-central1-a"
}
data "enos_environment" "current" {}
resource "random_string" "test_string" {
length = 5
lower = true
upper = false
numeric = false
special = false
}
resource "google_compute_network" "boundary_compute_network" {
name = "boundary-enos-network-${random_string.test_string.result}"
}
resource "random_id" "filter_label1" {
prefix = "enos_boundary"
byte_length = 4
}
resource "random_id" "filter_label2" {
prefix = "enos_boundary"
byte_length = 4
}
resource "tls_private_key" "ssh" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "google_compute_address" "boundary_external_ip" {
count = var.target_count
name = "boundary-external-ip-${random_string.test_string.result}-${count.index}"
address_type = "EXTERNAL"
}
resource "google_compute_firewall" "boundary_private_ssh" {
name = "boundary-private-ssh-${random_string.test_string.result}"
network = google_compute_network.boundary_compute_network.name
source_ranges = var.private_cidr_block
target_tags = ["boundary-target-${random_string.test_string.result}"]
allow {
protocol = "tcp"
ports = ["22"]
}
}
resource "google_compute_firewall" "boundary_enos_ssh" {
name = "boundary-enos-ssh-${random_string.test_string.result}"
network = google_compute_network.boundary_compute_network.name
source_ranges = flatten([formatlist("%s/32", data.enos_environment.current.public_ipv4_addresses)])
target_tags = ["boundary-target-${random_string.test_string.result}"]
allow {
protocol = "tcp"
ports = ["22"]
}
}
resource "google_compute_instance" "boundary_target" {
count = var.target_count
name = "boundary-target-${random_string.test_string.result}-${count.index}"
machine_type = var.instance_type
zone = var.gcp_zone
boot_disk {
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2204-lts"
}
}
network_interface {
network = google_compute_network.boundary_compute_network.id
access_config {
nat_ip = google_compute_address.boundary_external_ip[count.index].address
}
}
tags = ["boundary-target-${random_string.test_string.result}"]
metadata = {
ssh-keys = "ubuntu:${tls_private_key.ssh.public_key_openssh}"
}
labels = merge(var.additional_labels, {
"name" : "boundary-target-${random_string.test_string.result}-${count.index}",
"type" : "target",
"project" : "enos",
"project_name" : "qti-enos-boundary",
"environment" : var.environment,
"enos_user" : var.enos_user,
"filter_label_1" : random_id.filter_label1.hex
"filter_label_2" : random_id.filter_label2.hex
})
}
output "target_private_ips" {
value = [for instance in google_compute_instance.boundary_target : instance.network_interface[0].network_ip]
}
output "target_public_ips" {
value = [for instance in google_compute_instance.boundary_target : instance.network_interface[0].access_config[0].nat_ip]
}
output "target_ips" {
value = flatten([
[for instance in google_compute_instance.boundary_target : instance.network_interface[0].network_ip],
[for instance in google_compute_instance.boundary_target : instance.network_interface[0].access_config[0].nat_ip]
])
}
output "target_ssh_key" {
value = tls_private_key.ssh.private_key_pem
sensitive = true
}
output "filter_label1" {
value = "labels.filter_label_1=${random_id.filter_label1.hex}"
}
output "filter_label2" {
value = "labels.filter_label_2=${random_id.filter_label2.hex}"
}
+66
View File
@@ -222,6 +222,63 @@ variable "test_timeout" {
type = string
default = "25m"
}
variable "gcp_private_key_id" {
description = "ID of the private key used to authenticate with GCP"
type = string
sensitive = true
default = ""
}
variable "gcp_private_key" {
description = "Private key used to authenticate with GCP"
type = string
sensitive = true
default = ""
}
variable "gcp_project_id" {
description = "GCP project where the resources will be created"
type = string
default = ""
}
variable "gcp_zone" {
description = "GCP zone where the resources will be created"
type = string
default = ""
}
variable "gcp_target_ssh_key" {
description = "SSH key used to authenticate with GCP target"
type = string
sensitive = true
default = ""
}
variable "gcp_client_email" {
description = "GCP client email associated with the private key"
type = string
sensitive = true
default = ""
}
variable "gcp_host_set_filter1" {
description = "value for the first filter in the host set"
type = string
default = ""
}
variable "gcp_host_set_filter2" {
description = "value for the second filter in the host set"
type = string
default = ""
}
variable "gcp_host_set_ips" {
description = "List of IP addresses"
type = list(string)
default = [""]
}
resource "enos_local_exec" "get_go_version" {
count = var.go_version == "" ? 1 : 0
@@ -284,6 +341,15 @@ resource "enos_local_exec" "run_e2e_test" {
E2E_LDAP_USER_NAME = var.ldap_user_name
E2E_LDAP_USER_PASSWORD = var.ldap_user_password
E2E_LDAP_GROUP_NAME = var.ldap_group_name
E2E_GCP_PRIVATE_KEY_ID = var.gcp_private_key_id
E2E_GCP_PRIVATE_KEY = var.gcp_private_key
E2E_GCP_PROJECT_ID = var.gcp_project_id
E2E_GCP_CLIENT_EMAIL = var.gcp_client_email
E2E_GCP_ZONE = var.gcp_zone
E2E_GCP_TARGET_SSH_KEY = var.gcp_target_ssh_key
E2E_GCP_HOST_SET_FILTER1 = var.gcp_host_set_filter1
E2E_GCP_HOST_SET_FILTER2 = var.gcp_host_set_filter2
E2E_GCP_HOST_SET_IPS = jsonencode(var.gcp_host_set_ips)
E2E_MAX_PAGE_SIZE = var.max_page_size
E2E_CONTROLLER_CONTAINER_NAME = var.controller_container_name
BOUNDARY_DIR = abspath(var.local_boundary_src_dir)
@@ -45,6 +45,15 @@ docker run \
-e "E2E_LDAP_USER_NAME=$E2E_LDAP_USER_NAME" \
-e "E2E_LDAP_USER_PASSWORD=$E2E_LDAP_USER_PASSWORD" \
-e "E2E_LDAP_GROUP_NAME=$E2E_LDAP_GROUP_NAME" \
-e "E2E_GCP_PRIVATE_KEY_ID=$E2E_GCP_PRIVATE_KEY_ID" \
-e "E2E_GCP_PRIVATE_KEY=$E2E_GCP_PRIVATE_KEY" \
-e "E2E_GCP_CLIENT_EMAIL=$E2E_GCP_CLIENT_EMAIL" \
-e "E2E_GCP_PROJECT_ID=$E2E_GCP_PROJECT_ID" \
-e "E2E_GCP_ZONE=$E2E_GCP_ZONE" \
-e "E2E_GCP_TARGET_SSH_KEY=$E2E_GCP_TARGET_SSH_KEY" \
-e "E2E_GCP_HOST_SET_FILTER1=$E2E_GCP_HOST_SET_FILTER1" \
-e "E2E_GCP_HOST_SET_FILTER2=$E2E_GCP_HOST_SET_FILTER2" \
-e "E2E_GCP_HOST_SET_IPS=$E2E_GCP_HOST_SET_IPS" \
-e "E2E_MAX_PAGE_SIZE=$E2E_MAX_PAGE_SIZE" \
-e "E2E_CONTROLLER_CONTAINER_NAME=$E2E_CONTROLLER_CONTAINER_NAME" \
--mount type=bind,src=$BOUNDARY_DIR,dst=/src/boundary/ \
+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
}