diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d2c0e27 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +> Description here + +### Fixes +> paste links to issues/tasks in project management +- []() + +### Features +> paste links to issues/tasks in project management +- []() + +### Change implications + +- dependencies added/changed? **yes (explain) / no** diff --git a/.github/workflows/terraform_lint.yaml b/.github/workflows/terraform_lint.yaml new file mode 100644 index 0000000..8a3c1b0 --- /dev/null +++ b/.github/workflows/terraform_lint.yaml @@ -0,0 +1,28 @@ +name: Terraform CI - Lint + +on: + [push] + +jobs: + tf_lint: + name: 'Terraform lint (on tf ${{ matrix.terraform_version }})' + runs-on: ubuntu-latest + strategy: + matrix: + terraform_version: [ '~1.5.0', '~1.6.0', '~1.7.0', 'latest'] # in theory, we go down to 1.3, but I think that's overkill for lint + permissions: + contents: 'read' + id-token: 'write' + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: 'setup Terraform' + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ matrix.terraform_version }} + + - name: 'lint Terraform code' + # see https://www.terraform.io/cli/commands/fmt + run: + terraform fmt -check -recursive -diff . diff --git a/.github/workflows/terraform_validate.yml b/.github/workflows/terraform_validate.yml new file mode 100644 index 0000000..df34dfe --- /dev/null +++ b/.github/workflows/terraform_validate.yml @@ -0,0 +1,42 @@ +name: Terraform CI - validate + +on: + [push] + +jobs: + tf_validate: + name: 'Terraform validate (on tf ${{ matrix.terraform_version }})' + runs-on: ubuntu-latest + strategy: + matrix: + terraform_version: ['~1.3.0', '~1.4.0', '~1.5.0', '~1.6.0', '~1.7.0', 'latest'] + permissions: + contents: 'read' + id-token: 'write' + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: 'setup Terraform' + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ matrix.terraform_version }} + + - name: 'Terraform - validate examples/create_psoxy_connections' + working-directory: examples/create_psoxy_connections + run: | + terraform init + terraform validate + + - name: 'Terraform - validate modules/cognito_tenant_api_auth' + working-directory: modules/cognito_tenant_api_auth + run: | + terraform init + terraform validate + + - name: 'Terraform - validate modules/psoxy_connection' + working-directory: modules/psoxy_connection + run: | + terraform init + terraform validate \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b016ffc --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +**/*.tfstate +**/*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* diff --git a/README.md b/README.md index c635ab2..619553e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ -# terraform-aws-worklytics -Terraform module for AWS resources to support Worklytics Data Exports, and Tenant API access. +# Terraform AWS Worklytics + +A collection of Terraform submodules to support the Worklytics Tenant API using Amazon Web Services (AWS) resources. + +- [Tenant API: authentication via AWS Cognito](modules/cognito_tenant_api_auth) +- [Tenant API: create a Psoxy Connection](modules/psoxy_connection) + +The main use-case for these submodules is to create [Psoxy Connections] in Worklytics via its [Tenant API]. + +After a successful Psoxy deployment (Data Source fully configured, Psoxy instance tested and running), the last step +should be to create the actual connection in the [Data Sources] section of the Worklytics Web App. These submodules +provide a way to automate this process. + +See the [examples/create_psoxy_connections] for a complete usage example. + +[Psoxy Connections]: https://docs.worklytics.co/psoxy +[Tenant API]: https://docs.worklytics.co/knowledge-base/tenant-api +[Data Sources]: https://app.worklytics.co/analytics/data-sources +[examples/create_psoxy_connections]: examples/create_psoxy_connections \ No newline at end of file diff --git a/examples/create_psoxy_connections/.gitignore b/examples/create_psoxy_connections/.gitignore new file mode 100644 index 0000000..752a9ea --- /dev/null +++ b/examples/create_psoxy_connections/.gitignore @@ -0,0 +1,8 @@ + +# ignore variable files for examples; assume people are testing in dev or something +**/terraform.tfvars + +# ignore .terraform state/lock files; again, presume people are playing with examples in dev or something +**/.terraform +**/.terraform.lock.hcl +**/TODO** diff --git a/examples/create_psoxy_connections/README.md b/examples/create_psoxy_connections/README.md new file mode 100644 index 0000000..8048a93 --- /dev/null +++ b/examples/create_psoxy_connections/README.md @@ -0,0 +1,29 @@ +# Create Psoxy Connections Example + +This example illustrates how to create Psoxy connections in Worklytics via its Tenant API. + +Assuming you've set up a Psoxy instance in AWS, the example will create an AWS Cognito Identity Pool to authenticate +with the Tenant API, and a shell script that creates a Psoxy Connection for each one of the Data Sources configured in +your Psoxy instance. + +Terraform variables used: + +```hcl +worklytics_tenant_id = "116863361842113328137" +user_principal_email = "johndoe@acme.com" +psoxy_connections = [{ + integration = "data-source-psoxy", + kind = "aws", + endpoint = "https://acme.execute-api.us-east-1.amazonaws.com/data-source-psoxy/", + region = "us-east-1", + role_arn = "psoxy::caller::arn" +}] +``` + +- `worklytics_tenant_id` is the unique ID of your Worklytics tenant (obtain from Worklytics Web App). +- `user_principal_email` is the email of the user principal that will be used to authenticate with the Tenant API, +and must be registered as `DataConnectionAdmin` via the Worklytics Web App. +- `psoxy_connections` is a collection of the attributes for each Data Source configured in your Psoxy instance. + +**Once the Terraform script is executed, and the shell script is created, make sure that the user principal email is +registered as `DataConnectionAdmin` in the Worklytics Web App.** diff --git a/examples/create_psoxy_connections/main.tf b/examples/create_psoxy_connections/main.tf new file mode 100644 index 0000000..a809ef6 --- /dev/null +++ b/examples/create_psoxy_connections/main.tf @@ -0,0 +1,37 @@ +# create the resources needed to auth with the Worklytics Tenant API +module "tenant_api_auth" { + source = "../../modules/cognito_tenant_api_auth" + + resource_name_prefix = var.resource_name_prefix + worklytics_tenant_id = var.worklytics_tenant_id + user_principal_email = var.user_principal_email + tenant_api_host = var.tenant_api_host +} + +# create script files for each Psoxy connection +module "create_psoxy_connection_script" { + source = "../../modules/psoxy_connection" + + identity_pool_id = module.tenant_api_auth.worklytics_identity_pool_id + identity_pool_region = module.tenant_api_auth.worklytics_identity_pool_region + identity_pool_user_principal = module.tenant_api_auth.worklytics_identity_pool_user_principal + + for_each = { + for psoxy_connection in var.psoxy_connections : + psoxy_connection.integration => psoxy_connection + } + psoxy_connection = { + integration = each.value.integration + region = each.value.region + role_arn = each.value.role_arn + endpoint = each.value.endpoint + bucket = each.value.bucket + parser_id = each.value.parser_id + github_organization = each.value.github_organization + } + psoxy_connection_script_path = coalesce(var.psoxy_connection_script_path, path.module) + psoxy_connection_script_filename = "create_${each.value.integration}-${index(var.psoxy_connections, each.value) + 1}_connection.sh" + + tenant_api_host = var.tenant_api_host + worklytics_tenant_id = var.worklytics_tenant_id +} \ No newline at end of file diff --git a/examples/create_psoxy_connections/outputs.tf b/examples/create_psoxy_connections/outputs.tf new file mode 100644 index 0000000..f2cabc7 --- /dev/null +++ b/examples/create_psoxy_connections/outputs.tf @@ -0,0 +1,7 @@ +output "worklytics_tenant_api_identity_pool_user_principal" { + value = module.tenant_api_auth.worklytics_identity_pool_user_principal +} + +output "worklytics_tenant_api_scripts" { + value = { for key, value in module.create_psoxy_connection_script : key => value.worklytics_tenant_api_script_file.filename } +} \ No newline at end of file diff --git a/examples/create_psoxy_connections/variables.tf b/examples/create_psoxy_connections/variables.tf new file mode 100644 index 0000000..949718f --- /dev/null +++ b/examples/create_psoxy_connections/variables.tf @@ -0,0 +1,50 @@ +variable "resource_name_prefix" { + type = string + description = "Prefix to give to names of infra created by this module, where applicable." + default = "worklytics-tenant-api-" +} + +variable "worklytics_tenant_id" { + type = string + description = "Numeric ID of your Worklytics tenant's service account (obtain from Worklytics Web App)." + + validation { + condition = var.worklytics_tenant_id == null || can(regex("^\\d{21}$", var.worklytics_tenant_id)) + error_message = "`worklytics_tenant_id` must be a 21-digit numeric value. (or `null`, for pre-production use case where you don't want external entity to be allowed to assume the role)." + } +} + +variable "tenant_api_host" { + type = string + description = "Host of the Worklytics Tenant API: the domain by which Cognito will refer users." + default = "intl.worklytics.co" +} + +variable "region" { + type = string + description = "The AWS region where Cognito Identity Pool is created." + default = "us-east-1" +} + +variable "user_principal_email" { + type = string + description = "The email of the user that has been granted access to the Worklytics Tenant API (configure in Worklytics Web App)." +} + +variable "psoxy_connections" { + type = list(object({ + integration = string # The integration ID to use for this connection. + region = string # The AWS region of the Proxy instance. + role_arn = string # The ARN role to invoke the Proxy instance. + endpoint = optional(string) # The endpoint of the lambda function (Work Data connections use-case). + bucket = optional(string) # The S3 bucket (Bulk Data connections use-case). + parser_id = optional(string) # Bulk Data connections only. + github_organization = optional(string) # GitHub Connections only. + })) + description = "The connection details for Psoxy connections to be created via Worklytics Tenant API." +} + +variable "psoxy_connection_script_path" { + type = string + description = "Where to create the script to create the Psoxy connection" +} \ No newline at end of file diff --git a/modules/cognito_tenant_api_auth/README.md b/modules/cognito_tenant_api_auth/README.md new file mode 100644 index 0000000..dcefa08 --- /dev/null +++ b/modules/cognito_tenant_api_auth/README.md @@ -0,0 +1,5 @@ +# Module AWS Cognito Tenant API Auth + +This module creates an AWS Cognito Identity Pool to authenticate with the Worklytics Tenant API. + +See the [examples/create_psoxy_connections](../../examples/create_psoxy_connections) for a complete usage example. \ No newline at end of file diff --git a/modules/cognito_tenant_api_auth/main.tf b/modules/cognito_tenant_api_auth/main.tf new file mode 100644 index 0000000..d73f498 --- /dev/null +++ b/modules/cognito_tenant_api_auth/main.tf @@ -0,0 +1,38 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + } +} + +provider "aws" { + region = var.region +} + +resource "aws_cognito_identity_pool" "worklytics_identity_pool" { + identity_pool_name = "${var.resource_name_prefix}identity-pool" + allow_unauthenticated_identities = false + allow_classic_flow = false + developer_provider_name = var.tenant_api_host +} + +resource "aws_iam_policy" "cognito_developer_identities" { + name = "${aws_cognito_identity_pool.worklytics_identity_pool.identity_pool_name}_CognitoDeveloperIdentityForWorklyticsTerraform" + description = "Allow principal to read and lookup developer identities from Cognito Identity: ${aws_cognito_identity_pool.worklytics_identity_pool.id}" + + policy = jsonencode( + { + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : [ + "cognito-identity:GetOpenIdTokenForDeveloperIdentity", + ], + "Effect" : "Allow", + "Resource" : aws_cognito_identity_pool.worklytics_identity_pool.arn + } + ] + }) +} diff --git a/modules/cognito_tenant_api_auth/outputs.tf b/modules/cognito_tenant_api_auth/outputs.tf new file mode 100644 index 0000000..269b159 --- /dev/null +++ b/modules/cognito_tenant_api_auth/outputs.tf @@ -0,0 +1,15 @@ +output "worklytics_identity_pool_id" { + value = aws_cognito_identity_pool.worklytics_identity_pool.id +} + +output "worklytics_identity_pool_arn" { + value = aws_cognito_identity_pool.worklytics_identity_pool.arn +} + +output "worklytics_identity_pool_region" { + value = var.region +} + +output "worklytics_identity_pool_user_principal" { + value = var.user_principal_email +} diff --git a/modules/cognito_tenant_api_auth/variables.tf b/modules/cognito_tenant_api_auth/variables.tf new file mode 100644 index 0000000..dac48d6 --- /dev/null +++ b/modules/cognito_tenant_api_auth/variables.tf @@ -0,0 +1,36 @@ +variable "resource_name_prefix" { + type = string + description = "Prefix to give to names of infra created by this module, where applicable." + default = "worklytics-tenant-api-" +} + +variable "worklytics_tenant_id" { + type = string + description = "Numeric ID of your Worklytics tenant's service account (obtain from Worklytics Web App)." + + validation { + condition = var.worklytics_tenant_id == null || can(regex("^\\d{21}$", var.worklytics_tenant_id)) + error_message = "`worklytics_tenant_id` must be a 21-digit numeric value. (or `null`, for pre-production use case where you don't want external entity to be allowed to assume the role)." + } +} + +# This will appear in the AWS console (Amazon Cognito > Identity Pools > Identity Pool Name) as a "Linked login" in +# identity browser of the pool. In development, don't use `localhost:8080` as this is used as the value for +# `developer_provider_name` pool which is subject to some restrictions: only alphanumeric characters, dots, underscores +# and hyphens +variable "tenant_api_host" { + type = string + description = "Host of the Worklytics Tenant API: the domain by which Cognito will refer users." + default = "intl.worklytics.co" +} + +variable "region" { + type = string + description = "The AWS region where Cognito Identity Pool is created." + default = "us-east-1" +} + +variable "user_principal_email" { + type = string + description = "The email of the user that has been granted access to the Worklytics Tenant API (configure in Worklytics Web App)." +} diff --git a/modules/psoxy_connection/README.md b/modules/psoxy_connection/README.md new file mode 100644 index 0000000..7b2a004 --- /dev/null +++ b/modules/psoxy_connection/README.md @@ -0,0 +1,6 @@ +# Module Psoxy Connection + +This module creates a shell script that creates Psoxy Connections via the Worklytics Tenant API. It requires +an [AWS Cognito Identity Pool for authentication](../cognito_tenant_api_auth). + +See the [examples/create_psoxy_connections](../../examples/create_psoxy_connections) for a complete usage example. \ No newline at end of file diff --git a/modules/psoxy_connection/main.tf b/modules/psoxy_connection/main.tf new file mode 100644 index 0000000..135351f --- /dev/null +++ b/modules/psoxy_connection/main.tf @@ -0,0 +1,42 @@ + +locals { + psoxy_connection_script_path = "${coalesce(var.psoxy_connection_script_path, path.module)}/" + psoxy_connection_script_filename = coalesce(var.psoxy_connection_script_filename, "create_${var.psoxy_connection.integration}_connection.sh") + settings = merge( + { + PROXY_DEPLOYMENT_KIND = "AWS" + PROXY_AWS_REGION = var.psoxy_connection.region + PROXY_AWS_ROLE_ARN = var.psoxy_connection.role_arn + }, + var.psoxy_connection.bucket != null ? { PROXY_BUCKET = var.psoxy_connection.bucket } : {}, + var.psoxy_connection.endpoint != null ? { PROXY_ENDPOINT = var.psoxy_connection.endpoint } : {}, + var.psoxy_connection.parser_id != null ? { parserId = var.psoxy_connection.parser_id } : {}, + var.psoxy_connection.github_organization != null ? { GITHUB_ORGANIZATION = var.psoxy_connection.github_organization } : {} + ) + json_payload = jsonencode({ + integrationId = var.psoxy_connection.integration, + settings = local.settings + }) +} + +resource "local_file" "worklytics_tenant_api_script_file" { + content = <