diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 6d3ecbf00a..bc4604be85 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -51,6 +51,20 @@ module "read_license" { source = "./modules/read_license" } +module "vault_agent" { + source = "./modules/vault_agent" + + vault_install_dir = var.vault_install_dir + vault_instance_count = var.vault_instance_count +} + + +module "vault_verify_agent_output" { + source = "./modules/vault_verify_agent_output" + + vault_instance_count = var.vault_instance_count +} + module "vault_cluster" { source = "app.terraform.io/hashicorp-qti/aws-vault/enos" # source = "../../terraform-enos-aws-vault" diff --git a/enos/enos-scenario-agent.hcl b/enos/enos-scenario-agent.hcl new file mode 100644 index 0000000000..81cd12c4a1 --- /dev/null +++ b/enos/enos-scenario-agent.hcl @@ -0,0 +1,219 @@ +scenario "agent" { + matrix { + arch = ["amd64", "arm64"] + artifact_source = ["local", "crt", "artifactory"] + distro = ["ubuntu", "rhel"] + edition = ["oss", "ent"] + } + + terraform_cli = terraform_cli.default + terraform = terraform.default + providers = [ + provider.aws.default, + provider.enos.ubuntu, + provider.enos.rhel + ] + + locals { + build_tags = { + "oss" = ["ui"] + "ent" = ["enterprise", "ent"] + } + bundle_path = matrix.artifact_source != "artifactory" ? abspath(var.vault_bundle_path) : null + dependencies_to_install = ["jq"] + enos_provider = { + rhel = provider.enos.rhel + ubuntu = provider.enos.ubuntu + } + install_artifactory_artifact = local.bundle_path == null + tags = merge({ + "Project Name" : var.project_name + "Project" : "Enos", + "Environment" : "ci" + }, var.tags) + vault_instance_types = { + amd64 = "t3a.small" + arm64 = "t4g.small" + } + vault_instance_type = coalesce(var.vault_instance_type, local.vault_instance_types[matrix.arch]) + } + + step "get_local_metadata" { + skip_step = matrix.artifact_source != "local" + module = module.get_local_metadata + } + + step "build_vault" { + module = "build_${matrix.artifact_source}" + + variables { + build_tags = try(var.vault_local_build_tags, local.build_tags[matrix.edition]) + bundle_path = local.bundle_path + goarch = matrix.arch + goos = "linux" + artifactory_host = matrix.artifact_source == "artifactory" ? var.artifactory_host : null + artifactory_repo = matrix.artifact_source == "artifactory" ? var.artifactory_repo : null + artifactory_username = matrix.artifact_source == "artifactory" ? var.artifactory_username : null + artifactory_token = matrix.artifact_source == "artifactory" ? var.artifactory_token : null + arch = matrix.artifact_source == "artifactory" ? matrix.arch : null + vault_product_version = var.vault_product_version + artifact_type = matrix.artifact_source == "artifactory" ? var.vault_artifact_type : null + distro = matrix.artifact_source == "artifactory" ? matrix.distro : null + edition = matrix.artifact_source == "artifactory" ? matrix.edition : null + instance_type = matrix.artifact_source == "artifactory" ? local.vault_instance_type : null + revision = var.vault_revision + } + } + + step "find_azs" { + module = module.az_finder + + variables { + instance_type = [ + var.backend_instance_type, + local.vault_instance_type + ] + } + } + + step "create_vpc" { + module = module.create_vpc + + variables { + ami_architectures = [matrix.arch] + availability_zones = step.find_azs.availability_zones + common_tags = local.tags + } + } + + step "read_license" { + skip_step = matrix.edition == "oss" + module = module.read_license + + variables { + file_name = abspath(joinpath(path.root, "./support/vault.hclic")) + } + } + + step "create_backend_cluster" { + module = "backend_raft" + depends_on = [step.create_vpc] + + providers = { + enos = provider.enos.ubuntu + } + + variables { + ami_id = step.create_vpc.ami_ids["ubuntu"][matrix.arch] + common_tags = local.tags + instance_type = var.backend_instance_type + kms_key_arn = step.create_vpc.kms_key_arn + vpc_id = step.create_vpc.vpc_id + } + } + + step "create_vault_cluster" { + module = module.vault_cluster + depends_on = [ + step.create_backend_cluster, + step.build_vault, + ] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + variables { + ami_id = step.create_vpc.ami_ids[matrix.distro][matrix.arch] + common_tags = local.tags + consul_cluster_tag = step.create_backend_cluster.consul_cluster_tag + dependencies_to_install = local.dependencies_to_install + instance_type = local.vault_instance_type + kms_key_arn = step.create_vpc.kms_key_arn + storage_backend = "raft" + unseal_method = "shamir" + vault_local_artifact_path = local.bundle_path + vault_artifactory_release = local.install_artifactory_artifact ? step.build_vault.vault_artifactory_release : null + vault_license = matrix.edition != "oss" ? step.read_license.license : null + vpc_id = step.create_vpc.vpc_id + } + } + + step "start_vault_agent" { + module = "vault_agent" + depends_on = [ + step.create_backend_cluster, + step.build_vault, + step.create_vault_cluster, + ] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + variables { + vault_instances = step.create_vault_cluster.vault_instances + vault_root_token = step.create_vault_cluster.vault_root_token + vault_agent_template_destination = "/tmp/agent_output.txt" + vault_agent_template_contents = "{{ with secret \\\"auth/token/lookup-self\\\" }}orphan={{ .Data.orphan }} display_name={{ .Data.display_name }}{{ end }}" + } + } + + step "verify_vault_agent_output" { + module = module.vault_verify_agent_output + depends_on = [ + step.create_vault_cluster, + step.start_vault_agent, + ] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + variables { + vault_instances = step.create_vault_cluster.vault_instances + vault_agent_template_destination = "/tmp/agent_output.txt" + vault_agent_expected_output = "orphan=true display_name=approle" + } + } + + output "vault_cluster_instance_ids" { + description = "The Vault cluster instance IDs" + value = step.create_vault_cluster.instance_ids + } + + output "vault_cluster_pub_ips" { + description = "The Vault cluster public IPs" + value = step.create_vault_cluster.instance_public_ips + } + + output "vault_cluster_priv_ips" { + description = "The Vault cluster private IPs" + value = step.create_vault_cluster.instance_private_ips + } + + output "vault_cluster_key_id" { + description = "The Vault cluster Key ID" + value = step.create_vault_cluster.key_id + } + + output "vault_cluster_root_token" { + description = "The Vault cluster root token" + value = step.create_vault_cluster.vault_root_token + } + + output "vault_cluster_unseal_keys_b64" { + description = "The Vault cluster unseal keys" + value = step.create_vault_cluster.vault_unseal_keys_b64 + } + + output "vault_cluster_unseal_keys_hex" { + description = "The Vault cluster unseal keys hex" + value = step.create_vault_cluster.vault_unseal_keys_hex + } + + output "vault_cluster_tag" { + description = "The Vault cluster tag" + value = step.create_vault_cluster.vault_cluster_tag + } +} diff --git a/enos/modules/vault_agent/main.tf b/enos/modules/vault_agent/main.tf new file mode 100644 index 0000000000..001a53278a --- /dev/null +++ b/enos/modules/vault_agent/main.tf @@ -0,0 +1,67 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + enos = { + source = "app.terraform.io/hashicorp-qti/enos" + } + } +} + +variable "vault_agent_template_destination" { + type = string + description = "The destination of the template rendered by Agent" +} + +variable "vault_agent_template_contents" { + type = string + description = "The template contents to be rendered by Agent" +} + +variable "vault_root_token" { + type = string + description = "The Vault root token" +} + +variable "vault_instances" { + type = map(object({ + private_ip = string + public_ip = string + })) + description = "The Vault cluster instances that were created" +} + +variable "vault_instance_count" { + type = number + description = "How many vault instances are in the cluster" +} + +variable "vault_install_dir" { + type = string + description = "The directory where the Vault binary will be installed" +} + +locals { + vault_instances = { + for idx in range(var.vault_instance_count) : idx => { + public_ip = values(var.vault_instances)[idx].public_ip + private_ip = values(var.vault_instances)[idx].private_ip + } + } +} + +resource "enos_remote_exec" "set_up_approle_auth_and_agent" { + content = templatefile("${path.module}/templates/set-up-approle-and-agent.sh", { + vault_install_dir = var.vault_install_dir + vault_token = var.vault_root_token + vault_agent_template_destination = var.vault_agent_template_destination + vault_agent_template_contents = var.vault_agent_template_contents + }) + + transport = { + ssh = { + host = local.vault_instances[0].public_ip + } + } +} diff --git a/enos/modules/vault_agent/templates/set-up-approle-and-agent.sh b/enos/modules/vault_agent/templates/set-up-approle-and-agent.sh new file mode 100644 index 0000000000..5444508de3 --- /dev/null +++ b/enos/modules/vault_agent/templates/set-up-approle-and-agent.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +set -e + +binpath=${vault_install_dir}/vault + +fail() { + echo "$1" 1>&2 + return 1 +} + +test -x "$binpath" || fail "unable to locate vault binary at $binpath" + +export VAULT_ADDR='http://127.0.0.1:8200' +export VAULT_TOKEN='${vault_token}' + +# If approle was already enabled, disable it as we're about to re-enable it (the || true is so we don't fail if it doesn't already exist) +$binpath auth disable approle || true + +approle_create_status=$($binpath auth enable approle) + +approle_status=$($binpath write auth/approle/role/agent-role secret_id_ttl=700h token_num_uses=1000 token_ttl=600h token_max_ttl=700h secret_id_num_uses=1000) + +ROLEID=$($binpath read --format=json auth/approle/role/agent-role/role-id | jq -r '.data.role_id') + +if [[ "$ROLEID" == '' ]]; then + fail "expected ROLEID to be nonempty, but it is empty" +fi + +SECRETID=$($binpath write -f --format=json auth/approle/role/agent-role/secret-id | jq -r '.data.secret_id') + +if [[ "$SECRETID" == '' ]]; then + fail "expected SECRETID to be nonempty, but it is empty" +fi + +echo $ROLEID > /tmp/role-id +echo $SECRETID > /tmp/secret-id + +cat > /tmp/vault-agent.hcl <<- EOM +pid_file = "/tmp/pidfile" + +vault { + address = "http://127.0.0.1:8200" + tls_skip_verify = true + retry { + num_retries = 10 + } +} + +cache { + enforce_consistency = "always" + use_auto_auth_token = true +} + +listener "tcp" { + address = "127.0.0.1:8100" + tls_disable = true +} + +template { + destination = "${vault_agent_template_destination}" + contents = "${vault_agent_template_contents}" + exec { + command = "pkill -F /tmp/pidfile" + } +} + +auto_auth { + method { + type = "approle" + config = { + role_id_file_path = "/tmp/role-id" + secret_id_file_path = "/tmp/secret-id" + } + } + sink { + type = "file" + config = { + path = "/tmp/token" + } + } +} +EOM + +# If Agent is still running from a previous run, kill it +pkill -F /tmp/pidfile || true + +# If the template file already exists, remove it +rm ${vault_agent_template_destination} || true + +# Run agent (it will kill itself when it finishes rendering the template) +$binpath agent -config=/tmp/vault-agent.hcl > /tmp/agent-logs.txt 2>&1 diff --git a/enos/modules/vault_verify_agent_output/main.tf b/enos/modules/vault_verify_agent_output/main.tf new file mode 100644 index 0000000000..6643c8b626 --- /dev/null +++ b/enos/modules/vault_verify_agent_output/main.tf @@ -0,0 +1,53 @@ +terraform { + required_providers { + enos = { + source = "app.terraform.io/hashicorp-qti/enos" + } + } +} + +variable "vault_agent_template_destination" { + type = string + description = "The destination of the template rendered by Agent" +} + +variable "vault_agent_expected_output" { + type = string + description = "The output that's expected in the rendered template at vault_agent_template_destination" +} + +variable "vault_instance_count" { + type = number + description = "How many vault instances are in the cluster" +} + +variable "vault_instances" { + type = map(object({ + private_ip = string + public_ip = string + })) + description = "The vault cluster instances that were created" +} + +locals { + vault_instances = { + for idx in range(var.vault_instance_count) : idx => { + public_ip = values(var.vault_instances)[idx].public_ip + private_ip = values(var.vault_instances)[idx].private_ip + } + } +} + +resource "enos_remote_exec" "verify_vault_agent_output" { + content = templatefile("${path.module}/templates/verify-vault-agent-output.sh", { + vault_agent_template_destination = var.vault_agent_template_destination + vault_agent_expected_output = var.vault_agent_expected_output + vault_instances = jsonencode(local.vault_instances) + }) + + transport = { + ssh = { + host = local.vault_instances[0].public_ip + } + } +} diff --git a/enos/modules/vault_verify_agent_output/templates/verify-vault-agent-output.sh b/enos/modules/vault_verify_agent_output/templates/verify-vault-agent-output.sh new file mode 100644 index 0000000000..3c434ba972 --- /dev/null +++ b/enos/modules/vault_verify_agent_output/templates/verify-vault-agent-output.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +fail() { + echo "$1" 1>&2 + return 1 +} + +actual_output=$(cat ${vault_agent_template_destination}) +if [[ "$actual_output" != "${vault_agent_expected_output}" ]]; then + fail "expected '${vault_agent_expected_output}' to be the Agent output, but got: '$actual_output'" +fi