From 172381f2735aedd7d608876b005f922ad8af0f68 Mon Sep 17 00:00:00 2001 From: Michael Li Date: Wed, 31 Aug 2022 12:00:33 -0400 Subject: [PATCH 1/2] refact(enos): Move modules to its own file Enos allows module definitions to be in any file. This keeps all of the modules in one place (and also follows the model used by Vault) --- enos/enos-modules.hcl | 68 +++++++++++++++++++++++++++++++++++++++++ enos/enos.hcl | 70 ------------------------------------------- 2 files changed, 68 insertions(+), 70 deletions(-) create mode 100644 enos/enos-modules.hcl diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl new file mode 100644 index 000000000..6abcbe229 --- /dev/null +++ b/enos/enos-modules.hcl @@ -0,0 +1,68 @@ +module "az_finder" { + source = "./modules/az_finder" +} + +module "boundary" { + source = "app.terraform.io/hashicorp-qti/aws-boundary/enos" + version = ">= 0.2.6" + + project_name = "qti-enos-boundary" + environment = var.environment + common_tags = { + "Project" : "Enos", + "Project Name" : "qti-enos-boundary", + "Enos User" : var.enos_user, + "Environment" : var.environment + } + + ssh_aws_keypair = var.aws_ssh_keypair_name + alb_listener_api_port = var.alb_listener_api_port +} + +module "bats_deps" { + source = "./modules/bats_deps" +} + +module "build_crt" { + source = "./modules/build_crt" +} + +module "build_local" { + source = "./modules/build_local" +} + +module "infra" { + source = "app.terraform.io/hashicorp-qti/aws-infra/enos" + version = ">= 0.3.1" + + project_name = "qti-enos-boundary" + environment = var.environment + common_tags = { + "Project" : "Enos", + "Project Name" : "qti-enos-boundary", + "Enos User" : var.enos_user, + "Environment" : var.environment + } +} + +module "random_stringifier" { + source = "./modules/random_stringifier" +} + +module "target" { + source = "./modules/target" + target_count = var.target_count + + project_name = "qti-enos-boundary" + environment = var.environment + enos_user = var.enos_user +} + +module "test_smoke" { + source = "./modules/test_smoke" +} + +module "test_cli_ui" { + source = "./modules/test_cli_ui" +} + diff --git a/enos/enos.hcl b/enos/enos.hcl index 0423b6738..bb3d12d06 100644 --- a/enos/enos.hcl +++ b/enos/enos.hcl @@ -33,76 +33,6 @@ provider "enos" "default" { } } -module "az_finder" { - source = "./modules/az_finder" -} - -module "boundary" { - #source = "../../terraform-enos-aws-boundary" - source = "app.terraform.io/hashicorp-qti/aws-boundary/enos" - version = ">= 0.2.6" - - project_name = "qti-enos-boundary" - environment = var.environment - common_tags = { - "Project" : "Enos", - "Project Name" : "qti-enos-boundary", - "Enos User" : var.enos_user, - "Environment" : var.environment - } - - ssh_aws_keypair = var.aws_ssh_keypair_name - alb_listener_api_port = var.alb_listener_api_port -} - -module "bats_deps" { - source = "./modules/bats_deps" -} - -module "build_crt" { - source = "./modules/build_crt" -} - -module "build_local" { - source = "./modules/build_local" -} - -module "infra" { - # source = "../../../terraform-enos-aws-infra" - source = "app.terraform.io/hashicorp-qti/aws-infra/enos" - version = ">= 0.3.1" - - project_name = "qti-enos-boundary" - environment = var.environment - common_tags = { - "Project" : "Enos", - "Project Name" : "qti-enos-boundary", - "Enos User" : var.enos_user, - "Environment" : var.environment - } -} - -module "random_stringifier" { - source = "./modules/random_stringifier" -} - -module "target" { - source = "./modules/target" - target_count = var.target_count - - project_name = "qti-enos-boundary" - environment = var.environment - enos_user = var.enos_user -} - -module "test_smoke" { - source = "./modules/test_smoke" -} - -module "test_cli_ui" { - source = "./modules/test_cli_ui" -} - scenario "integration" { terraform_cli = terraform_cli.default terraform = terraform.default From 21e4b70e27d88287aa56dd8409eb6536300bb2af Mon Sep 17 00:00:00 2001 From: Michael Li Date: Wed, 31 Aug 2022 12:08:46 -0400 Subject: [PATCH 2/2] test(e2e): Add end-to-end tests (connect to first target) This adds the initial end-to-end tests. These tests validate the admin workflow of setting up their first target --- enos/README.md | 5 + enos/enos-modules.hcl | 3 + enos/enos-scenario-e2e-target.hcl | 110 +++++++ enos/modules/test_e2e/main.tf | 62 ++++ go.mod | 1 + go.sum | 2 + testing/internal/e2e/README.md | 42 +++ testing/internal/e2e/boundary/boundary.go | 101 +++++++ testing/internal/e2e/helpers.go | 55 ++++ testing/internal/e2e/target/target_test.go | 316 +++++++++++++++++++++ 10 files changed, 697 insertions(+) create mode 100644 enos/enos-scenario-e2e-target.hcl create mode 100644 enos/modules/test_e2e/main.tf create mode 100644 testing/internal/e2e/README.md create mode 100644 testing/internal/e2e/boundary/boundary.go create mode 100644 testing/internal/e2e/helpers.go create mode 100644 testing/internal/e2e/target/target_test.go diff --git a/enos/README.md b/enos/README.md index 5724f5889..6a16ef7f7 100644 --- a/enos/README.md +++ b/enos/README.md @@ -111,3 +111,8 @@ you could test that version against the cluster by setting `local_boundary_dir` in the Boundary cluster and then executes the Bats CLI UI tests against it. This scenario requires the machine executing `enos` to be configured for the Bats tests as described in the Requirements section. + +## End-to-end tests + +Scenarios with `e2e_` invoke an end-to-end test suite written in Go. Different tests +are invoked depending on the scenario. diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 6abcbe229..2fc4c0dac 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -66,3 +66,6 @@ module "test_cli_ui" { source = "./modules/test_cli_ui" } +module "test_e2e" { + source = "./modules/test_e2e" +} diff --git a/enos/enos-scenario-e2e-target.hcl b/enos/enos-scenario-e2e-target.hcl new file mode 100644 index 000000000..08bab9ed2 --- /dev/null +++ b/enos/enos-scenario-e2e-target.hcl @@ -0,0 +1,110 @@ +scenario "e2e_target" { + terraform_cli = terraform_cli.default + terraform = terraform.default + providers = [ + provider.aws.default, + provider.enos.default + ] + + matrix { + builder = ["local", "crt"] + } + + locals { + aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) + boundary_install_dir = abspath(var.boundary_install_dir) + local_boundary_dir = abspath(var.local_boundary_dir) + build_path = { + "local" = "/tmp", + "crt" = var.crt_bundle_path == null ? null : abspath(var.crt_bundle_path) + } + } + + step "find_azs" { + module = module.az_finder + + variables { + instance_type = [ + var.worker_instance_type, + var.controller_instance_type + ] + } + } + + step "create_db_password" { + module = module.random_stringifier + } + + step "build_boundary" { + module = matrix.builder == "crt" ? module.build_crt : module.build_local + + variables { + path = local.build_path[matrix.builder] + } + } + + step "create_base_infra" { + module = module.infra + + variables { + availability_zones = step.find_azs.availability_zones + } + } + + step "create_boundary_cluster" { + module = module.boundary + depends_on = [ + step.create_base_infra, + step.build_boundary + ] + + variables { + boundary_install_dir = local.boundary_install_dir + controller_instance_type = var.controller_instance_type + controller_count = var.controller_count + db_pass = step.create_db_password.string + kms_key_arn = step.create_base_infra.kms_key_arn + local_artifact_path = step.build_boundary.artifact_path + ubuntu_ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] + vpc_id = step.create_base_infra.vpc_id + worker_count = var.worker_count + worker_instance_type = var.worker_instance_type + } + } + + step "launch_targets" { + module = module.target + depends_on = [step.create_base_infra] + + variables { + ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] + aws_ssh_keypair_name = var.aws_ssh_keypair_name + enos_user = var.enos_user + instance_type = var.target_instance_type + vpc_id = step.create_base_infra.vpc_id + } + } + + step "run_e2e_targets_test" { + module = module.test_e2e + depends_on = [ + step.create_boundary_cluster, + step.launch_targets + ] + + variables { + test_package = "github.com/hashicorp/boundary/testing/internal/e2e/target" + alb_boundary_api_addr = step.create_boundary_cluster.alb_boundary_api_addr + auth_method_id = step.create_boundary_cluster.auth_method_id + auth_login_name = step.create_boundary_cluster.auth_login_name + auth_password = step.create_boundary_cluster.auth_password + local_boundary_dir = local.local_boundary_dir + aws_ssh_private_key_path = local.aws_ssh_private_key_path + target_ips = step.launch_targets.target_ips + } + } + + output "test_results" { + value = step.run_e2e_targets_test.test_results + } +} diff --git a/enos/modules/test_e2e/main.tf b/enos/modules/test_e2e/main.tf new file mode 100644 index 000000000..00cbf418d --- /dev/null +++ b/enos/modules/test_e2e/main.tf @@ -0,0 +1,62 @@ +terraform { + required_providers { + enos = { + source = "app.terraform.io/hashicorp-qti/enos" + } + } +} + +variable "test_package" { + description = "Name of Go test package to run" + type = string +} +variable "alb_boundary_api_addr" { + description = "URL of the Boundary instance" + type = string +} +variable "auth_method_id" { + description = "Id of Auth Method used to login to Boundary instance" + type = string +} +variable "auth_login_name" { + description = "Name of admin user" + type = string +} +variable "auth_password" { + description = "Password of admin user" + type = string +} +variable "local_boundary_dir" { + description = "Local Path to boundary executable" + type = string +} +variable "aws_ssh_private_key_path" { + description = "Local Path to key used to SSH onto created hosts" + type = string +} +variable "target_ips" { + description = "List of IP Addresses of created hosts" + type = list(string) +} + +locals { + aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) +} + +resource "enos_local_exec" "run_e2e_test" { + environment = { + BOUNDARY_ADDR = var.alb_boundary_api_addr, + E2E_PASSWORD_AUTH_METHOD_ID = var.auth_method_id, + E2E_PASSWORD_ADMIN_LOGIN_NAME = var.auth_login_name, + E2E_PASSWORD_ADMIN_PASSWORD = var.auth_password, + E2E_TARGET_IP = var.target_ips[0], + E2E_SSH_USER = "ubuntu" + E2E_SSH_KEY_PATH = local.aws_ssh_private_key_path, + } + + inline = ["PATH=\"${var.local_boundary_dir}:$PATH\" go test -v ${var.test_package}"] +} + +output "test_results" { + value = enos_local_exec.run_e2e_test.stdout +} diff --git a/go.mod b/go.mod index 7f0b8615f..d993b9240 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220711120347-32232bae6803 github.com/hashicorp/nodeenrollment v0.1.16 + github.com/kelseyhightower/envconfig v1.4.0 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e ) diff --git a/go.sum b/go.sum index 0c296c092..8f7969b4f 100644 --- a/go.sum +++ b/go.sum @@ -890,6 +890,8 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= diff --git a/testing/internal/e2e/README.md b/testing/internal/e2e/README.md new file mode 100644 index 000000000..e4332de2d --- /dev/null +++ b/testing/internal/e2e/README.md @@ -0,0 +1,42 @@ +# boundary-e2e-tests + +This test suite tests Boundary in an end-to-end setting using [Enos](https://github.com/hashicorp/Enos-Docs) to spin up the desired infrastructure and [go test](https://pkg.go.dev/testing) to perform user workflows. + +## Getting Started +### Usage +#### Enos +Setup Enos as described [here](../../enos/README.md) + +```shell +enos scenario run e2e_{scenario} builder:local // runs and destroys infra + +enos scenario launch e2e_{scenario} builder:local // runs and keeps infra online +enos scenario output // displays any defined enos output + +enos scenario destroy // destroys infra +``` + +Enos scenarios set up the infrastructure, set the appropriate environment variables, and run the selected tests. Folders in this directory correspond to an enos scenario (e.g. `enos/enos-scenario-e2e-target.hcl` runs tests in `testing/e2e/target`) + +#### Local +Set the appropriate environment variables... +```shell +export BOUNDARY_ADDR= # e.g. http://127.0.0.1:9200 +export E2E_PASSWORD_AUTH_METHOD_ID= # e.g. ampw_1234567890 +export E2E_PASSWORD_ADMIN_LOGIN_NAME= # e.g. "admin" +export E2E_PASSWORD_ADMIN_PASSWORD= # e.g. "password" + +# For e2e/target +export E2E_TARGET_IP= # e.g. 192.168.0.1 +export E2E_SSH_KEY_PATH= # e.g. /Users/username/key.pem +export E2E_SSH_USER= # e.g. ubuntu +export E2E_SSH_PORT= # e.g. 22 +``` + +Then, run... +```shell +go test github.com/hashicorp/boundary/testing/e2e/target // run target tests +go test ./target/ // run target tests if running from this directory +go test github.com/hashicorp/boundary/testing/e2e/target -v // verbose +go test github.com/hashicorp/boundary/testing/e2e/target -v -run '^TestCreateTargetApi$' // run a specific test +``` diff --git a/testing/internal/e2e/boundary/boundary.go b/testing/internal/e2e/boundary/boundary.go new file mode 100644 index 000000000..57022e9e7 --- /dev/null +++ b/testing/internal/e2e/boundary/boundary.go @@ -0,0 +1,101 @@ +// Package boundary provides methods for commonly used boundary actions that are used in end-to-end tests. +package boundary + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/kelseyhightower/envconfig" +) + +type config struct { + Address string `envconfig:"BOUNDARY_ADDR"` // e.g. http://127.0.0.1:9200 + AuthMethodId string `envconfig:"E2E_PASSWORD_AUTH_METHOD_ID"` // e.g. ampw_1234567890 + AdminLoginName string `envconfig:"E2E_PASSWORD_ADMIN_LOGIN_NAME" default:"admin"` + AdminLoginPassword string `envconfig:"E2E_PASSWORD_ADMIN_PASSWORD"` +} + +func (c *config) validate() error { + if c.Address == "" { + return errors.New("Address is empty. Set environment variable: BOUNDARY_ADDR") + } + if c.AuthMethodId == "" { + return errors.New("AuthMethodId is empty. Set environment variable: E2E_PASSWORD_AUTH_METHOD_ID") + } + if c.AdminLoginName == "" { + return errors.New("AdminLoginName is empty. Set environment variable: E2E_PASSWORD_ADMIN_LOGIN_NAME") + } + if c.AdminLoginPassword == "" { + return errors.New("AdminLoginPassword is empty. Set environment variable: E2E_PASSWORD_ADMIN_PASSWORD") + } + + return nil +} + +func loadConfig() (*config, error) { + var c config + err := envconfig.Process("", &c) + if err != nil { + return nil, err + } + + return &c, err +} + +// NewApiClient creates a new Api client for the specified Boundary instance and +// attempts to authenticate it. Returns the client. +func NewApiClient() (*api.Client, error) { + c, err := loadConfig() + if err != nil { + return nil, err + } + err = c.validate() + if err != nil { + return nil, err + } + + client, err := api.NewClient(&api.Config{Addr: c.Address}) + if err != nil { + return nil, err + } + + ctx := context.Background() + authmethodsClient := authmethods.NewClient(client) + authenticationResult, err := authmethodsClient.Authenticate(ctx, c.AuthMethodId, "login", + map[string]interface{}{ + "login_name": c.AdminLoginName, + "password": c.AdminLoginPassword, + }, + ) + if err != nil { + return nil, err + } + + client.SetToken(fmt.Sprint(authenticationResult.Attributes["token"])) + return client, err +} + +// AuthenticateCli uses the cli to authenticate the specified Boundary instance. +// Returns the result of the command. +func AuthenticateCli() *e2e.CommandResult { + c, err := loadConfig() + if err != nil { + return &e2e.CommandResult{Err: err} + } + err = c.validate() + if err != nil { + return &e2e.CommandResult{Err: err} + } + + return e2e.RunCommand( + "boundary", "authenticate", "password", + "-addr", c.Address, + "-auth-method-id", c.AuthMethodId, + "-login-name", c.AdminLoginName, + "-password", "env://E2E_PASSWORD_ADMIN_PASSWORD", + ) +} diff --git a/testing/internal/e2e/helpers.go b/testing/internal/e2e/helpers.go new file mode 100644 index 000000000..4d38f35e3 --- /dev/null +++ b/testing/internal/e2e/helpers.go @@ -0,0 +1,55 @@ +package e2e + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "testing" +) + +// CommandResult encapsulates the output from running an external command +type CommandResult struct { + Stdout []byte + Stderr []byte + ExitCode int + Err error +} + +const EnvToCheckSkip = "E2E_PASSWORD_AUTH_METHOD_ID" + +// RunCommand executes external commands on the system. Returns the results +// of running the provided command. CommandResult is always valid even if there is +// an error. +func RunCommand(name string, args ...string) *CommandResult { + var outbuf, errbuf bytes.Buffer + + cmd := exec.Command(name, args...) + cmd.Stdout = &outbuf + cmd.Stderr = &errbuf + + err := cmd.Run() + + var ee *exec.ExitError + var exitCode int + if errors.As(err, &ee) { + exitCode = ee.ExitCode() + } + + return &CommandResult{ + Stdout: outbuf.Bytes(), + Stderr: errbuf.Bytes(), + ExitCode: exitCode, + Err: err, + } +} + +func MaybeSkipTest(t *testing.T) { + if _, ok := os.LookupEnv(EnvToCheckSkip); !ok { + t.Skip(fmt.Sprintf( + "Skipping test because environment variable '%s' is not set. This is needed for e2e tests.", + EnvToCheckSkip, + )) + } +} diff --git a/testing/internal/e2e/target/target_test.go b/testing/internal/e2e/target/target_test.go new file mode 100644 index 000000000..194097d80 --- /dev/null +++ b/testing/internal/e2e/target/target_test.go @@ -0,0 +1,316 @@ +package target_test + +import ( + "context" + "encoding/json" + "errors" + "strings" + "testing" + + "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/api/targets" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/kelseyhightower/envconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type config struct { + TargetIp string `envconfig:"E2E_TARGET_IP"` // e.g. 192.168.0.1 + TargetSshKeyPath string `envconfig:"E2E_SSH_KEY_PATH"` // e.g. /Users/username/key.pem + TargetSshUser string `envconfig:"E2E_SSH_USER"` // e.g. ubuntu + TargetPort string `envconfig:"E2E_SSH_PORT" default:"22"` +} + +func (c *config) validate() error { + if c.TargetIp == "" { + return errors.New("TargetIp is empty. Set environment variable: E2E_TARGET_IP") + } + if c.TargetSshKeyPath == "" { + return errors.New("TargetSshKeyPath is empty. Set environment variable: E2E_SSH_KEY_PATH") + } + if c.TargetSshUser == "" { + return errors.New("TargetSshUser is empty. Set environment variable: E2E_SSH_USER") + } + if c.TargetPort == "" { + return errors.New("TargetPort is empty. Set environment variable: E2E_SSH_PORT") + } + + return nil +} + +func loadConfig() (*config, error) { + var c config + err := envconfig.Process("", &c) + if err != nil { + return nil, err + } + + return &c, err +} + +// TestConnectTargetCli uses the boundary cli to create a number of supporting objects +// to connect to a target. It then attempts to connect to that target and verifies that +// the connection was successful. +func TestConnectTargetCli(t *testing.T) { + e2e.MaybeSkipTest(t) + + c, err := loadConfig() + require.NoError(t, err) + err = c.validate() + require.NoError(t, err) + + output := boundary.AuthenticateCli() + require.NoError(t, output.Err, string(output.Stderr)) + + // Create an org + output = e2e.RunCommand( + "boundary", "scopes", "create", + "-name", "e2e Automated Test Org", + "-format", "json", + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newOrgResult scopes.ScopeCreateResult + err = json.Unmarshal(output.Stdout, &newOrgResult) + require.NoError(t, err) + newOrg := newOrgResult.Item + t.Cleanup(func() { + output := e2e.RunCommand("boundary", "scopes", "delete", "-id", newOrg.Id) + require.NoError(t, output.Err, string(output.Stderr)) + }) + t.Logf("Created Org Id: %s", newOrg.Id) + + // Create a project + output = e2e.RunCommand( + "boundary", "scopes", "create", + "-scope-id", newOrg.Id, + "-name", "e2e Automated Test Project", + "-format", "json", + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newProjectResult scopes.ScopeCreateResult + err = json.Unmarshal(output.Stdout, &newProjectResult) + require.NoError(t, err) + newProject := newProjectResult.Item + t.Cleanup(func() { + output := e2e.RunCommand("boundary", "scopes", "delete", "-id", newProject.Id) + require.NoError(t, output.Err, string(output.Stderr)) + }) + t.Logf("Created Project Id: %s", newProject.Id) + + // Create a host catalog + output = e2e.RunCommand( + "boundary", "host-catalogs", "create", "static", + "-scope-id", newProject.Id, + "-name", "e2e Automated Test Host Catalog", + "-format", "json", + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newHostCatalogResult hostcatalogs.HostCatalogCreateResult + err = json.Unmarshal(output.Stdout, &newHostCatalogResult) + require.NoError(t, err) + newHostCatalog := newHostCatalogResult.Item + t.Cleanup(func() { + output := e2e.RunCommand("boundary", "host-catalogs", "delete", "-id", newHostCatalog.Id) + require.NoError(t, output.Err, string(output.Stderr)) + }) + t.Logf("Created Host Catalog: %s", newHostCatalog.Id) + + // Create a host set and add to catalog + output = e2e.RunCommand( + "boundary", "host-sets", "create", "static", + "-host-catalog-id", newHostCatalog.Id, + "-name", "e2e Automated Test Host Set", + "-format", "json", + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newHostSetResult hostsets.HostSetCreateResult + err = json.Unmarshal(output.Stdout, &newHostSetResult) + require.NoError(t, err) + newHostSet := newHostSetResult.Item + t.Cleanup(func() { + output := e2e.RunCommand("boundary", "host-sets", "delete", "-id", newHostSet.Id) + require.NoError(t, output.Err, string(output.Stderr)) + }) + t.Logf("Created Host Set: %s", newHostSet.Id) + + // Create a host + output = e2e.RunCommand( + "boundary", "hosts", "create", "static", + "-host-catalog-id", newHostCatalog.Id, + "-name", c.TargetIp, + "-address", c.TargetIp, + "-format", "json", + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newHostResult hosts.HostCreateResult + err = json.Unmarshal(output.Stdout, &newHostResult) + require.NoError(t, err) + newHost := newHostResult.Item + t.Cleanup(func() { + output := e2e.RunCommand("boundary", "hosts", "delete", "-id", newHost.Id) + require.NoError(t, output.Err, string(output.Stderr)) + }) + t.Logf("Created Host: %s", newHost.Id) + + // Add host to host set + output = e2e.RunCommand( + "boundary", "host-sets", "add-hosts", + "-id", newHostSet.Id, + "-host", newHost.Id, + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create a target + output = e2e.RunCommand( + "boundary", "targets", "create", "tcp", + "-scope-id", newProject.Id, + "-default-port", c.TargetPort, + "-name", "e2e Automated Test Target", + "-format", "json", + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newTargetResult targets.TargetCreateResult + err = json.Unmarshal(output.Stdout, &newTargetResult) + require.NoError(t, err) + newTarget := newTargetResult.Item + t.Cleanup(func() { + output := e2e.RunCommand("boundary", "targets", "delete", "-id", newTarget.Id) + require.NoError(t, output.Err, string(output.Stderr)) + }) + t.Logf("Created Target: %s", newTarget.Id) + + // Add host set to target + output = e2e.RunCommand( + "boundary", "targets", "add-host-sources", + "-id", newTarget.Id, + "-host-source", newHostSet.Id, + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Connect to target and print host's IP address + output = e2e.RunCommand( + "boundary", "connect", + "-target-id", newTarget.Id, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-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] + assert.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") +} + +// TestCreateTargetApi uses the boundary go api to create a number of supporting objects +// to connect to a target. This test does not connect to the target due to the complexity +// when not using the cli. +func TestCreateTargetApi(t *testing.T) { + e2e.MaybeSkipTest(t) + + c, err := loadConfig() + require.NoError(t, err) + err = c.validate() + require.NoError(t, err) + + client, err := boundary.NewApiClient() + require.NoError(t, err) + + ctx := context.Background() + + // Create an org + scopeClient := scopes.NewClient(client) + newOrgResult, err := scopeClient.Create(ctx, "global", scopes.WithName("e2e Automated Test Org")) + require.NoError(t, err) + newOrg := newOrgResult.Item + t.Cleanup(func() { + _, err := scopeClient.Delete(ctx, newOrg.Id) + require.NoError(t, err) + }) + t.Logf("Created Org Id: %s", newOrg.Id) + + // Create a project + newProjectResult, err := scopeClient.Create(ctx, newOrg.Id, scopes.WithName("e2e Automated Test Project")) + require.NoError(t, err) + newProject := newProjectResult.Item + t.Cleanup(func() { + _, err := scopeClient.Delete(ctx, newProject.Id) + require.NoError(t, err) + }) + t.Logf("Created Project Id: %s", newProject.Id) + + // Create a host catalog + hcClient := hostcatalogs.NewClient(client) + newHostCatalogResult, err := hcClient.Create(ctx, "static", newProject.Id, + hostcatalogs.WithName("e2e Automated Test Host Catalog"), + ) + require.NoError(t, err) + newHostCatalog := newHostCatalogResult.Item + t.Cleanup(func() { + _, err := hcClient.Delete(ctx, newHostCatalog.Id) + require.NoError(t, err) + }) + t.Logf("Created Host Catalog: %s", newHostCatalog.Id) + + // Create a host set and add to catalog + hsClient := hostsets.NewClient(client) + newHostSetResult, err := hsClient.Create(ctx, newHostCatalog.Id) + require.NoError(t, err) + newHostSet := newHostSetResult.Item + t.Cleanup(func() { + _, err := hsClient.Delete(ctx, newHostSet.Id) + require.NoError(t, err) + }) + t.Logf("Created Host Set: %s", newHostSet.Id) + + // Create a host + hClient := hosts.NewClient(client) + newHostResult, err := hClient.Create(ctx, newHostCatalog.Id, + hosts.WithName(c.TargetIp), + hosts.WithStaticHostAddress(c.TargetIp), + ) + require.NoError(t, err) + newHost := newHostResult.Item + t.Cleanup(func() { + _, err := hClient.Delete(ctx, newHost.Id) + require.NoError(t, err) + }) + t.Logf("Created Host: %s", newHost.Id) + + // Add host to host set + _, err = hsClient.AddHosts(ctx, newHostSet.Id, 0, []string{newHost.Id}, hostsets.WithAutomaticVersioning(true)) + require.NoError(t, err) + + // Create a target + tClient := targets.NewClient(client) + newTargetResult, err := tClient.Create(ctx, "tcp", newProject.Id, + targets.WithName("e2e Automated Test Target"), + targets.WithTcpTargetDefaultPort(22), + ) + require.NoError(t, err) + newTarget := newTargetResult.Item + t.Cleanup(func() { + _, err := tClient.Delete(ctx, newTarget.Id) + require.NoError(t, err) + }) + t.Logf("Created Target: %s", newTarget.Id) + + // Add host set to target + _, err = tClient.AddHostSources(ctx, newTarget.Id, 0, + []string{newHostSet.Id}, + targets.WithAutomaticVersioning(true), + ) + require.NoError(t, err) +}