diff --git a/ci/pipeline.yml b/ci/pipeline.yml new file mode 100644 index 0000000..6e2b872 --- /dev/null +++ b/ci/pipeline.yml @@ -0,0 +1,411 @@ +jobs: + - name: set-self + plan: + - get: src + trigger: true + - set_pipeline: self + file: src/ci/pipeline.yml + + - name: terraform-plan-apps-development + plan: + - in_parallel: + - get: terraform-templates + resource: terraform-config + trigger: true + - get: src + resource: src + trigger: false + passed: [set-self] + # Changes to the iaas state file trigger a build. This is not a step + # input because the state is accessed separately using a + # terraform_remote_state data source. + - get: terraform-yaml + resource: terraform-yaml-development + trigger: true + - get: pipeline-tasks + - get: general-task + - get: csb-image + trigger: true + - get: csb-docproxy-image + trigger: true + - load_var: csb-image-repository + file: csb-image/repository + - load_var: csb-image-digest + file: csb-image/digest + - load_var: csb-docproxy-image-repository + file: csb-docproxy-image/repository + - load_var: csb-docproxy-image-digest + file: csb-docproxy-image/digest + - task: terraform-plan + image: general-task + file: terraform-templates/ci/terraform/terraform-apply.yml + params: &tf-apps-development + TERRAFORM_ACTION: plan + TEMPLATE_SUBDIR: terraform/stacks/apps + STACK_NAME: cf-apps-development + S3_TFSTATE_BUCKET: ((tf-state-bucket)) + AWS_DEFAULT_REGION: ((aws-region)) + CF_API_URL: ((cf-api-url-development)) + CF_CLIENT_ID: ((cf-client-id-development)) + CF_CLIENT_SECRET: ((cf-client-secret-development)) + TF_VAR_csb_aws_region_commercial: ((csb-aws-region-commercial)) + TF_VAR_csb_aws_region_govcloud: ((aws-region)) + TF_VAR_csb_aws_ses_default_zone: appmail.dev.us-gov-west-1.aws-us-gov.cloud.gov + TF_VAR_csb_broker_route_domain: ((csb-broker-route-domain-development)) + TF_VAR_csb_docker_image_name: "((.:csb-image-repository))" + TF_VAR_csb_docker_image_version: "@((.:csb-image-digest))" + TF_VAR_csb_docproxy_docker_image_name: "((.:csb-docproxy-image-repository))" + TF_VAR_csb_docproxy_docker_image_version: "@((.:csb-docproxy-image-digest))" + TF_VAR_csb_docproxy_domain: dev.us-gov-west-1.aws-us-gov.cloud.gov + TF_VAR_csb_org_name: ((csb-org-name)) + TF_VAR_csb_space_name: ((csb-space-name)) + TF_VAR_external_remote_state_reader_access_key_id: ((development-tf-state-access-key-id)) + TF_VAR_external_remote_state_reader_region: ((development-tf-state-region)) + TF_VAR_external_remote_state_reader_secret_access_key: ((development-tf-state-secret-access-key)) + TF_VAR_external_stack_name: external-development + TF_VAR_remote_state_bucket_external: ((tf-state-bucket-external)) + TF_VAR_remote_state_bucket_iaas: ((tf-state-bucket)) + TF_VAR_stack_name: development + - put: slack + params: + text_file: terraform-state/message.txt + text: | + :terraform: $BUILD_JOB_NAME needs review + <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> + channel: "#cg-customer-success" + username: ((slack-username)) + icon_url: ((slack-icon-url)) + + - name: terraform-apply-apps-development + plan: + - in_parallel: + - get: terraform-templates + resource: terraform-config + passed: [terraform-plan-apps-development] + trigger: true + - get: pipeline-tasks + - get: general-task + - get: csb-image + trigger: true + - get: csb-docproxy-image + trigger: true + - load_var: csb-image-repository + file: csb-image/repository + - load_var: csb-image-digest + file: csb-image/digest + - load_var: csb-docproxy-image-repository + file: csb-docproxy-image/repository + - load_var: csb-docproxy-image-digest + file: csb-docproxy-image/digest + - task: terraform-apply + image: general-task + file: terraform-templates/ci/terraform/terraform-apply.yml + params: + <<: *tf-apps-development + TERRAFORM_ACTION: apply + + - name: terraform-plan-apps-staging + plan: + - in_parallel: + - get: terraform-templates + resource: terraform-config + trigger: true + passed: [terraform-apply-apps-development] + # Changes to the iaas state file trigger a build. This is not a step + # input because the state is accessed separately using a + # terraform_remote_state data source. + - get: terraform-yaml + resource: terraform-yaml-staging + trigger: true + - get: pipeline-tasks + - get: general-task + - get: csb-image + trigger: true + - get: csb-docproxy-image + trigger: true + - load_var: csb-image-repository + file: csb-image/repository + - load_var: csb-image-digest + file: csb-image/digest + - load_var: csb-docproxy-image-repository + file: csb-docproxy-image/repository + - load_var: csb-docproxy-image-digest + file: csb-docproxy-image/digest + - task: terraform-plan + image: general-task + file: terraform-templates/ci/terraform/terraform-apply.yml + params: &tf-apps-staging + TERRAFORM_ACTION: plan + TEMPLATE_SUBDIR: terraform/stacks/apps + STACK_NAME: cf-apps-staging + S3_TFSTATE_BUCKET: ((tf-state-bucket)) + AWS_DEFAULT_REGION: ((aws-region)) + CF_API_URL: ((cf-api-url-staging)) + CF_CLIENT_ID: ((cf-client-id-staging)) + CF_CLIENT_SECRET: ((cf-client-secret-staging)) + TF_VAR_csb_aws_region_commercial: ((csb-aws-region-commercial)) + TF_VAR_csb_aws_region_govcloud: ((aws-region)) + TF_VAR_csb_aws_ses_default_zone: appmail.fr-stage.cloud.gov + TF_VAR_csb_broker_route_domain: ((csb-broker-route-domain-staging)) + TF_VAR_csb_docker_image_name: "((.:csb-image-repository))" + TF_VAR_csb_docker_image_version: "@((.:csb-image-digest))" + TF_VAR_csb_docproxy_docker_image_name: "((.:csb-docproxy-image-repository))" + TF_VAR_csb_docproxy_docker_image_version: "@((.:csb-docproxy-image-digest))" + TF_VAR_csb_docproxy_domain: fr-stage.cloud.gov + TF_VAR_csb_org_name: ((csb-org-name)) + TF_VAR_csb_space_name: ((csb-space-name)) + TF_VAR_external_remote_state_reader_access_key_id: ((staging-tf-state-access-key-id)) + TF_VAR_external_remote_state_reader_region: ((staging-tf-state-region)) + TF_VAR_external_remote_state_reader_secret_access_key: ((staging-tf-state-secret-access-key)) + TF_VAR_external_stack_name: external-staging + TF_VAR_remote_state_bucket_external: ((tf-state-bucket-external)) + TF_VAR_remote_state_bucket_iaas: ((tf-state-bucket)) + TF_VAR_stack_name: staging + - put: slack + params: + text_file: terraform-state/message.txt + text: | + :terraform: $BUILD_JOB_NAME needs review + <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> + channel: "#cg-customer-success" + username: ((slack-username)) + icon_url: ((slack-icon-url)) + + - name: terraform-apply-apps-staging + plan: + - in_parallel: + - get: terraform-templates + resource: terraform-config + trigger: true + passed: [terraform-plan-apps-staging] + - get: pipeline-tasks + - get: general-task + - get: csb-image + trigger: true + - get: csb-docproxy-image + trigger: true + - load_var: csb-image-repository + file: csb-image/repository + - load_var: csb-image-digest + file: csb-image/digest + - load_var: csb-docproxy-image-repository + file: csb-docproxy-image/repository + - load_var: csb-docproxy-image-digest + file: csb-docproxy-image/digest + - task: terraform-apply + image: general-task + file: terraform-templates/ci/terraform/terraform-apply.yml + params: + <<: *tf-apps-staging + TERRAFORM_ACTION: apply + + - name: terraform-plan-apps-production + plan: + - in_parallel: + - get: terraform-templates + resource: terraform-config + passed: [terraform-apply-apps-staging] + trigger: true + # Changes to the iaas state file trigger a build. This is not a step + # input because the state is accessed separately using a + # terraform_remote_state data source. + - get: terraform-yaml + resource: terraform-yaml-production + trigger: true + - get: pipeline-tasks + - get: general-task + - get: csb-image + trigger: true + - get: csb-docproxy-image + trigger: true + - load_var: csb-image-repository + file: csb-image/repository + - load_var: csb-image-digest + file: csb-image/digest + - load_var: csb-docproxy-image-repository + file: csb-docproxy-image/repository + - load_var: csb-docproxy-image-digest + file: csb-docproxy-image/digest + - task: terraform-plan + image: general-task + file: terraform-templates/ci/terraform/terraform-apply.yml + params: &tf-apps-production + TERRAFORM_ACTION: plan + TEMPLATE_SUBDIR: terraform/stacks/apps + STACK_NAME: cf-apps-production + S3_TFSTATE_BUCKET: ((tf-state-bucket)) + AWS_DEFAULT_REGION: ((aws-region)) + CF_API_URL: ((cf-api-url-production)) + CF_CLIENT_ID: ((cf-client-id-production)) + CF_CLIENT_SECRET: ((cf-client-secret-production)) + TF_VAR_csb_aws_region_commercial: ((csb-aws-region-commercial)) + TF_VAR_csb_aws_region_govcloud: ((aws-region)) + TF_VAR_csb_aws_ses_default_zone: appmail.cloud.gov + TF_VAR_csb_broker_route_domain: ((csb-broker-route-domain-production)) + TF_VAR_csb_docker_image_name: "((.:csb-image-repository))" + TF_VAR_csb_docker_image_version: "@((.:csb-image-digest))" + TF_VAR_csb_docproxy_docker_image_name: "((.:csb-docproxy-image-repository))" + TF_VAR_csb_docproxy_docker_image_version: "@((.:csb-docproxy-image-digest))" + TF_VAR_csb_docproxy_domain: fr.cloud.gov + TF_VAR_csb_docproxy_instances: 2 + TF_VAR_csb_org_name: ((csb-org-name)) + TF_VAR_csb_space_name: ((csb-space-name)) + TF_VAR_external_remote_state_reader_access_key_id: ((production-tf-state-access-key-id)) + TF_VAR_external_remote_state_reader_region: ((production-tf-state-region)) + TF_VAR_external_remote_state_reader_secret_access_key: ((production-tf-state-secret-access-key)) + TF_VAR_external_stack_name: external-production + TF_VAR_remote_state_bucket_external: ((tf-state-bucket-external)) + TF_VAR_remote_state_bucket_iaas: ((tf-state-bucket)) + TF_VAR_stack_name: production + - put: slack + params: + text_file: terraform-state/message.txt + text: | + :terraform: $BUILD_JOB_NAME needs review + <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> + channel: "#cg-customer-success" + username: ((slack-username)) + icon_url: ((slack-icon-url)) + + - name: terraform-apply-apps-production + plan: + - in_parallel: + - get: terraform-templates + resource: terraform-config + passed: [terraform-plan-apps-production] + - get: pipeline-tasks + - get: general-task + - get: csb-image + trigger: true + - get: csb-docproxy-image + trigger: true + - load_var: csb-image-repository + file: csb-image/repository + - load_var: csb-image-digest + file: csb-image/digest + - load_var: csb-docproxy-image-repository + file: csb-docproxy-image/repository + - load_var: csb-docproxy-image-digest + file: csb-docproxy-image/digest + - task: terraform-apply + image: general-task + file: terraform-templates/ci/terraform/terraform-apply.yml + params: + <<: *tf-apps-production + TERRAFORM_ACTION: apply + +resources: + - name: pipeline-tasks + type: git + source: + commit_verification_keys: ((cloud-gov-pgp-keys)) + uri: https://github.com/cloud-gov/cg-pipeline-tasks.git + branch: main + + - name: slack + type: slack-notification + source: + url: ((slack-webhook-url)) + + - name: terraform-config + type: git + source: + commit_verification_keys: ((cloud-gov-pgp-keys)) + uri: https://github.com/cloud-gov/csb.git + branch: main + paths: + - ci/terraform/* + + - name: terraform-yaml-development + type: s3-iam + source: + bucket: ((tf-state-bucket)) + versioned_file: ((tf-state-file-development)) + region_name: ((aws-region)) + + - name: terraform-yaml-staging + type: s3-iam + source: + bucket: ((tf-state-bucket)) + versioned_file: ((tf-state-file-staging)) + region_name: ((aws-region)) + + - name: terraform-yaml-production + type: s3-iam + source: + bucket: ((tf-state-bucket)) + versioned_file: ((tf-state-file-production)) + region_name: ((aws-region)) + + - name: general-task + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: general-task + aws_region: us-gov-west-1 + tag: latest + + - name: csb-image + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: csb + aws_region: us-gov-west-1 + tag: latest + + - name: csb-docproxy-image + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: csb-docproxy + aws_region: us-gov-west-1 + tag: latest + + - name: src + type: git + source: + commit_verification_keys: ((cloud-gov-pgp-keys)) + uri: https://github.com/cloud-gov/csb.git + branch: main + paths: + - ci/* + +resource_types: + - name: registry-image + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: registry-image-resource + aws_region: us-gov-west-1 + tag: latest + + - name: slack-notification + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: slack-notification-resource + aws_region: us-gov-west-1 + tag: latest + + - name: git + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: git-resource + aws_region: us-gov-west-1 + tag: latest + + - name: s3-iam + type: registry-image + source: + aws_access_key_id: ((ecr_aws_key)) + aws_secret_access_key: ((ecr_aws_secret)) + repository: s3-resource + aws_region: us-gov-west-1 + tag: latest diff --git a/ci/terraform/module/csb.tf b/ci/terraform/module/csb.tf new file mode 100644 index 0000000..3dcf4cf --- /dev/null +++ b/ci/terraform/module/csb.tf @@ -0,0 +1,89 @@ +resource "random_password" "csb_app_password" { + length = 32 + special = false + min_special = 0 + min_upper = 5 + min_numeric = 5 + min_lower = 5 +} + +resource "cloudfoundry_app" "csb" { + name = "csb" + org_name = var.org_name + space_name = var.space_name + + docker_image = "${var.docker_image_name}${var.docker_image_version}" + docker_credentials = { + "username" = var.ecr_access_key_id + "password" = var.ecr_secret_access_key + } + + command = "/app/csb serve" + instances = var.instances + memory = "1G" + disk_quota = "7G" + + environment = { + # General broker configuration. + # Configuration spec: https://github.com/cloudfoundry/cloud-service-broker/blob/main/docs/configuration.md + BROKERPAK_UPDATES_ENABLED = true + DB_HOST = var.rds_host + DB_NAME = var.rds_name + DB_PASSWORD = var.rds_password + DB_PORT = var.rds_port + DB_TLS = true + DB_USERNAME = var.rds_name + SECURITY_USER_NAME = "broker" + SECURITY_USER_PASSWORD = random_password.csb_app_password.result + TERRAFORM_UPGRADES_ENABLED = true + + # Access keys for managing resources provisioned by brokerpaks + AWS_ACCESS_KEY_ID_GOVCLOUD = var.aws_access_key_id_govcloud + AWS_SECRET_ACCESS_KEY_GOVCLOUD = var.aws_secret_access_key_govcloud + AWS_REGION_GOVCLOUD = var.aws_region_govcloud + AWS_ACCESS_KEY_ID_COMMERCIAL = var.aws_access_key_id_commercial + AWS_SECRET_ACCESS_KEY_COMMERCIAL = var.aws_secret_access_key_commercial + AWS_REGION_COMMERCIAL = var.aws_region_commercial + + # Other values that are used by convention by all brokerpaks + CLOUD_GOV_ENVIRONMENT = var.stack_name + + # Brokerpak-specific variables + BP_AWS_SES_DEFAULT_ZONE = var.aws_ses_default_zone + } + + readiness_health_check_type = "http" + readiness_health_check_http_endpoint = "/ready" +} + +data "cloudfoundry_domain" "brokers_domain" { + name = var.broker_route_domain +} + +resource "cloudfoundry_route" "csb" { + space = data.cloudfoundry_space.brokers.id + domain = data.cloudfoundry_domain.brokers_domain.id + host = "csb" + + destinations = [{ + app_id = cloudfoundry_app.csb.id + }] +} + +resource "cloudfoundry_route" "csb_docs" { + space = data.cloudfoundry_space.brokers.id + domain = data.cloudfoundry_domain.brokers_domain.id + host = "csb" + path = "docs" + + destinations = [{ + app_id = cloudfoundry_app.csb.id + }] +} + +resource "cloudfoundry_service_broker" "csb" { + name = "csb" + password = random_password.csb_app_password.result + url = "https://${cloudfoundry_route.csb.url}" + username = "broker" +} diff --git a/ci/terraform/module/docproxy.tf b/ci/terraform/module/docproxy.tf new file mode 100644 index 0000000..8c9d247 --- /dev/null +++ b/ci/terraform/module/docproxy.tf @@ -0,0 +1,52 @@ +resource "cloudfoundry_app" "docproxy" { + name = "docproxy" + org_name = var.org_name + space_name = var.space_name + + docker_image = "${var.docproxy_docker_image_name}${var.docproxy_docker_image_version}" + docker_credentials = { + "username" = var.ecr_access_key_id + "password" = var.ecr_secret_access_key + } + + command = "/app/docproxy" + instances = var.docproxy_instances + memory = "128M" + + environment = { + "BROKER_URL" = cloudfoundry_route.csb.url + "PORT" = 8080 + } +} + +data "cloudfoundry_domain" "cloudgov_platform_domain" { + name = var.docproxy_domain +} + +resource "cloudfoundry_route" "docproxy" { + domain = data.cloudfoundry_domain.cloudgov_platform_domain.id + space = data.cloudfoundry_space.brokers.id + host = "services" + + destinations = [{ + app_id = cloudfoundry_app.docproxy.id + }] +} + +data "cloudfoundry_service_plans" "external_domain" { + service_offering_name = "external-domain" + name = "domain" + service_broker_name = "external-domain-broker" +} + +resource "cloudfoundry_service_instance" "docproxy_external_domain" { + name = "docproxy-domain" + space = data.cloudfoundry_space.brokers.id + type = "managed" + + service_plan = data.cloudfoundry_service_plans.external_domain.service_plans[0].id + + parameters = jsonencode({ + domains = ["services.${var.docproxy_domain}"] + }) +} diff --git a/ci/terraform/module/shared.tf b/ci/terraform/module/shared.tf new file mode 100644 index 0000000..7a9c7e0 --- /dev/null +++ b/ci/terraform/module/shared.tf @@ -0,0 +1,8 @@ +data "cloudfoundry_org" "platform" { + name = var.org_name +} + +data "cloudfoundry_space" "brokers" { + name = var.space_name + org = data.cloudfoundry_org.platform.id +} diff --git a/ci/terraform/module/variables.tf b/ci/terraform/module/variables.tf new file mode 100644 index 0000000..3472ffe --- /dev/null +++ b/ci/terraform/module/variables.tf @@ -0,0 +1,133 @@ +variable "stack_name" { + type = string + description = "Like development, staging, or production." +} + +# CSB CF Application Configuration + +variable "org_name" { + type = string + description = "The name of the Cloud Foundry organization in which the broker will be deployed." +} + +variable "space_name" { + type = string + description = "The name of the Cloud Foundry space in which the broker will be deployed." +} + +variable "docker_image_name" { + type = string + description = "Full name (but not tag or SHA) of the Docker image the broker will use." +} + +variable "docker_image_version" { + type = string + description = "Tag or SHA of the Docker image the broker will use. For example, ':latest' or '@sha256:abc123...'." + default = ":latest" +} + +variable "ecr_access_key_id" { + description = "For pulling the CSB image from ECR." + type = string +} + +variable "ecr_secret_access_key" { + description = "For pulling the CSB image from ECR." + sensitive = true + type = string +} + +variable "instances" { + description = "Number of instances of the CSB app to run." + type = number +} + +variable "broker_route_domain" { + type = string + description = "The domain under which the broker's route will be created. For example, 'fr.cloud.gov'." +} + +# Database credentials + +variable "rds_host" { + type = string + description = "Hostname of the RDS instance for the Cloud Service Broker." +} + +variable "rds_port" { + type = string + description = "Port of the RDS instance for the Cloud Service Broker." +} + +variable "rds_name" { + type = string + description = "Database name within the RDS instance for the Cloud Service Broker." +} + +variable "rds_username" { + type = string + description = "Database username of the RDS instance for the Cloud Service Broker." +} + +variable "rds_password" { + type = string + sensitive = true + description = "Database password of the RDS instance for the Cloud Service Broker." +} + +# CSB Configuration + +variable "aws_ses_default_zone" { + type = string + description = "When the user does not provide a domain, a subdomain will be created for them under this DNS zone." +} + +variable "aws_access_key_id_govcloud" { + type = string +} + +variable "aws_secret_access_key_govcloud" { + type = string + sensitive = true +} + +variable "aws_region_govcloud" { + type = string +} + +variable "aws_access_key_id_commercial" { + type = string +} + +variable "aws_secret_access_key_commercial" { + type = string + sensitive = true +} + +variable "aws_region_commercial" { + type = string +} + +# Docproxy configuration + +variable "docproxy_domain" { + type = string + description = "The parent domain in CF under which the docproxy will be routed. For example, to serve it on services.fr.cloud.gov, set this to fr.cloud.gov. The subdomain is always 'services'." +} + +variable "docproxy_docker_image_name" { + type = string + description = "Full name (but not tag or SHA) of the Docker image the broker will use." +} + +variable "docproxy_docker_image_version" { + type = string + description = "Tag or SHA of the Docker image the broker will use. For example, ':latest' or '@sha256:abc123...'." + default = ":latest" + +} + +variable "docproxy_instances" { + type = number + description = "Number of instances of the docproxy app to run." +} diff --git a/ci/terraform/module/versions.tf b/ci/terraform/module/versions.tf new file mode 100644 index 0000000..b7d3175 --- /dev/null +++ b/ci/terraform/module/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "< 2.0.0" + required_providers { + cloudfoundry = { + source = "cloudfoundry/cloudfoundry" + version = "< 2.0" + } + } +} diff --git a/ci/terraform/stack/apps.tf b/ci/terraform/stack/apps.tf new file mode 100644 index 0000000..006b06e --- /dev/null +++ b/ci/terraform/stack/apps.tf @@ -0,0 +1,35 @@ +module "csb" { + source = "../module" + + count = var.stack_name == "development" ? 1 : 0 + + stack_name = var.stack_name + + rds_host = data.terraform_remote_state.iaas.outputs.csb.rds.host + rds_port = data.terraform_remote_state.iaas.outputs.csb.rds.port + rds_name = data.terraform_remote_state.iaas.outputs.csb.rds.name + rds_username = data.terraform_remote_state.iaas.outputs.csb.rds.username + rds_password = data.terraform_remote_state.iaas.outputs.csb.rds.password + + ecr_access_key_id = data.terraform_remote_state.iaas.outputs.csb.ecr_user.access_key_id_curr + ecr_secret_access_key = data.terraform_remote_state.iaas.outputs.csb.ecr_user.secret_access_key_curr + instances = 1 + aws_ses_default_zone = var.csb_aws_ses_default_zone + aws_access_key_id_govcloud = data.terraform_remote_state.iaas.outputs.csb.broker_user.access_key_id_curr + aws_secret_access_key_govcloud = data.terraform_remote_state.iaas.outputs.csb.broker_user.secret_access_key_curr + aws_region_govcloud = var.csb_aws_region_govcloud + aws_access_key_id_commercial = data.terraform_remote_state.external.outputs.csb.broker_user.access_key_id_curr + aws_secret_access_key_commercial = data.terraform_remote_state.external.outputs.csb.broker_user.secret_access_key_curr + aws_region_commercial = var.csb_aws_region_commercial + + org_name = var.csb_org_name + space_name = var.csb_space_name + docker_image_name = var.csb_docker_image_name + docker_image_version = var.csb_docker_image_version + broker_route_domain = var.csb_broker_route_domain + + docproxy_domain = var.csb_docproxy_domain + docproxy_instances = var.csb_docproxy_instances + docproxy_docker_image_name = var.csb_docproxy_docker_image_name + docproxy_docker_image_version = var.csb_docproxy_docker_image_version +} diff --git a/ci/terraform/stack/data.tf b/ci/terraform/stack/data.tf new file mode 100644 index 0000000..3e4345c --- /dev/null +++ b/ci/terraform/stack/data.tf @@ -0,0 +1,18 @@ +data "terraform_remote_state" "iaas" { + backend = "s3" + config = { + bucket = var.remote_state_bucket_iaas + key = "${var.stack_name}/terraform.tfstate" + } +} + +data "terraform_remote_state" "external" { + backend = "s3" + config = { + access_key = var.external_remote_state_reader_access_key_id + secret_key = var.external_remote_state_reader_secret_access_key + region = var.external_remote_state_reader_region + bucket = var.remote_state_bucket_external + key = "${var.external_stack_name}/terraform.tfstate" + } +} diff --git a/ci/terraform/stack/providers.tf b/ci/terraform/stack/providers.tf new file mode 100644 index 0000000..c98c92d --- /dev/null +++ b/ci/terraform/stack/providers.tf @@ -0,0 +1,7 @@ +terraform { + backend "s3" { + } +} + +provider "cloudfoundry" { +} diff --git a/ci/terraform/stack/variables.tf b/ci/terraform/stack/variables.tf new file mode 100644 index 0000000..a926166 --- /dev/null +++ b/ci/terraform/stack/variables.tf @@ -0,0 +1,83 @@ +variable "stack_name" { + type = string + description = "One of development, staging, production." +} + +variable "remote_state_bucket_iaas" { + type = string + description = "Bucket where remote state for AWS GovCloud is stored." +} + +variable "remote_state_bucket_external" { + type = string + description = "Bucket where remote state for AWS Commercial is stored." +} + +variable "external_remote_state_reader_access_key_id" { + type = string + description = "Access key ID for the IAM user that has permission to read from the state bucket." +} + +variable "external_remote_state_reader_secret_access_key" { + type = string + sensitive = true + description = "Secret access key for the IAM user that has permission to read from the state bucket." +} + +variable "external_remote_state_reader_region" { + type = string + description = "The region in which the remote state bucket is located." +} + +variable "external_stack_name" { + type = string +} + +variable "csb_aws_region_govcloud" { + type = string +} + +variable "csb_aws_region_commercial" { + type = string +} + +variable "csb_aws_ses_default_zone" { + type = string +} + +variable "csb_docker_image_name" { + type = string +} + +variable "csb_docker_image_version" { + type = string +} + +variable "csb_org_name" { + type = string +} + +variable "csb_space_name" { + type = string +} + +variable "csb_broker_route_domain" { + type = string +} + +variable "csb_docproxy_domain" { + type = string +} + +variable "csb_docproxy_docker_image_name" { + type = string +} + +variable "csb_docproxy_docker_image_version" { + type = string +} + +variable "csb_docproxy_instances" { + type = number + default = 1 +} diff --git a/ci/terraform/stack/versions.tf b/ci/terraform/stack/versions.tf new file mode 100644 index 0000000..b7d3175 --- /dev/null +++ b/ci/terraform/stack/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "< 2.0.0" + required_providers { + cloudfoundry = { + source = "cloudfoundry/cloudfoundry" + version = "< 2.0" + } + } +} diff --git a/ci/terraform/terraform-apply.sh b/ci/terraform/terraform-apply.sh new file mode 100755 index 0000000..6b570a8 --- /dev/null +++ b/ci/terraform/terraform-apply.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -eu + +# Use client credentials in CF_CLIENT_ID and CF_CLIENT_SECRET to fetch a token +API_RESPONSE=$(curl -s $CF_API_URL/v2/info) +TOKEN_ENDPOINT=$(echo ${API_RESPONSE} | jq -r '.token_endpoint // empty') + +if [ -z "${TOKEN_ENDPOINT}" ]; then + echo "API didn't return a token endpoint: ${API_RESPONSE}" + exit 99; +fi + +UAA_RESPONSE=$(curl -s \ + -X POST \ + -d "grant_type=client_credentials&response_type=token&client_id=${CF_CLIENT_ID}&client_secret=${CF_CLIENT_SECRET}" \ + ${TOKEN_ENDPOINT}/oauth/token +) +export CF_TOKEN=$(echo ${UAA_RESPONSE} | jq -r -r '.access_token // empty') + +if [ -z "${CF_TOKEN}" ]; then + echo "UAA did not return a token: ${UAA_RESPONSE}" + exit 99; +fi + +# Execute the terraform action, the cloudfoundry provider will use CF_API and CF_TOKEN to authenticate +./pipeline-tasks/terraform-apply.sh diff --git a/ci/terraform/terraform-apply.yml b/ci/terraform/terraform-apply.yml new file mode 100644 index 0000000..a575569 --- /dev/null +++ b/ci/terraform/terraform-apply.yml @@ -0,0 +1,11 @@ +--- +platform: linux + +inputs: +- name: terraform-templates +- name: pipeline-tasks +outputs: +- name: terraform-state + +run: + path: terraform-templates/terraform/terraform-apply.sh diff --git a/docproxy/main.go b/docproxy/main.go index 8bd89b0..b6bb094 100644 --- a/docproxy/main.go +++ b/docproxy/main.go @@ -30,6 +30,7 @@ func walk(n *html.Node, f func(*html.Node) bool) bool { // modifyDocument makes a series of changes to the html.Node in-place. func modifyDocument(n *html.Node) { +ModifyDocument: modifications := []func(*html.Node) bool{ func(n *html.Node) bool { if n.Type == html.ElementNode && n.Data == "head" { @@ -102,6 +103,22 @@ func modifyDocument(n *html.Node) { } return false }, + func(n *html.Node) bool { + if n.Type == html.ElementNode && n.Data == "img" { + src := html.Attribute{ + Key: "src", + Val: "https://example.com/icon.jpg", + } + newSrc := html.Attribute{ + Key: "src", + Val: "images/amazon-ses.svg", + } + if i := slices.Index(n.Attr, src); i >= 0 { + n.Attr[i] = newSrc + } + } + return false + }, } walk(n, func(n *html.Node) bool { for _, m := range modifications { @@ -179,13 +196,17 @@ func routes(c config) { } type config struct { - Port uint16 + Host string + Port uint16 BrokerURL *url.URL } func loadConfig() (config, error) { c := config{} + // Host can be empty, for local development, a value like "localhost". + c.Host = os.Getenv("HOST") + port := os.Getenv("PORT") p, err := strconv.ParseUint(port, 10, 16) if err != nil { @@ -213,7 +234,7 @@ func run() error { } routes(config) - addr := fmt.Sprintf("localhost:%v", config.Port) + addr := fmt.Sprintf("%v:%v", config.Host, config.Port) slog.Info("Starting server...") return http.ListenAndServe(addr, nil) }