diff --git a/assets/docker/perforce/p4-broker/Dockerfile b/assets/docker/perforce/p4-broker/Dockerfile new file mode 100644 index 00000000..fcecb1c2 --- /dev/null +++ b/assets/docker/perforce/p4-broker/Dockerfile @@ -0,0 +1,18 @@ +FROM amazonlinux:2023 + +# Install p4broker from Perforce YUM repository +RUN rpm --import https://package.perforce.com/perforce.pubkey && \ + echo -e "[perforce]\nname=Perforce\nbaseurl=https://package.perforce.com/yum/rhel/9/x86_64\nenabled=1\ngpgcheck=1\ngpgkey=https://package.perforce.com/perforce.pubkey" \ + > /etc/yum.repos.d/perforce.repo && \ + dnf install -y helix-broker && \ + dnf clean all && \ + rm -rf /var/cache/dnf + +# Create config directory +RUN mkdir -p /config /tmp + +# p4broker listens on 1666 by default +EXPOSE 1666 + +# Run p4broker in foreground mode +ENTRYPOINT ["/usr/sbin/p4broker", "-c", "/config/p4broker.conf"] diff --git a/assets/docker/perforce/p4-broker/README.md b/assets/docker/perforce/p4-broker/README.md new file mode 100644 index 00000000..c7c86b27 --- /dev/null +++ b/assets/docker/perforce/p4-broker/README.md @@ -0,0 +1,56 @@ +# P4 Broker Container Image + +This directory contains the Dockerfile for building a Perforce Helix Broker (`p4broker`) container image. + +## Building the Image + +```bash +docker build -t p4-broker . +``` + +## Pushing to Amazon ECR + +```bash +# Authenticate with ECR +aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com + +# Tag the image +docker tag p4-broker:latest .dkr.ecr..amazonaws.com/p4-broker:latest + +# Push the image +docker push .dkr.ecr..amazonaws.com/p4-broker:latest +``` + +## Pushing to Another Container Registry + +```bash +# Tag the image for your registry +docker tag p4-broker:latest /p4-broker:latest + +# Push the image +docker push /p4-broker:latest +``` + +## Local Testing + +```bash +# Create a local broker config file +cat > p4broker.conf < [aws](#requirement\_aws) | ~> 6.6 | | [awscc](#requirement\_awscc) | ~> 1.51 | | [local](#requirement\_local) | ~> 2.5 | +| [netapp-ontap](#requirement\_netapp-ontap) | ~> 2.3 | | [null](#requirement\_null) | ~> 3.2 | | [random](#requirement\_random) | ~> 3.7 | @@ -162,6 +165,7 @@ packer build perforce_x86.pkr.hcl | Name | Source | Version | |------|--------|---------| | [p4\_auth](#module\_p4\_auth) | ./modules/p4-auth | n/a | +| [p4\_broker](#module\_p4\_broker) | ./modules/p4-broker | n/a | | [p4\_code\_review](#module\_p4\_code\_review) | ./modules/p4-code-review | n/a | | [p4\_server](#module\_p4\_server) | ./modules/p4-server | n/a | @@ -174,6 +178,7 @@ packer build perforce_x86.pkr.hcl | [aws_lb.perforce](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb) | resource | | [aws_lb.perforce_web_services](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb) | resource | | [aws_lb_listener.perforce](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | +| [aws_lb_listener.perforce_broker](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | | [aws_lb_listener.perforce_web_services](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | | [aws_lb_listener.perforce_web_services_http_listener](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | | [aws_lb_listener_rule.p4_code_review](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener_rule) | resource | @@ -190,12 +195,16 @@ packer build perforce_x86.pkr.hcl | [aws_s3_bucket_policy.shared_lb_access_logs_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | | [aws_security_group.perforce_network_load_balancer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.perforce_web_services_alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_vpc_security_group_egress_rule.p4_broker_outbound_to_p4_server](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | | [aws_vpc_security_group_egress_rule.p4_code_review_outbound_to_p4_server](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | | [aws_vpc_security_group_egress_rule.perforce_alb_outbound_to_p4_auth](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | | [aws_vpc_security_group_egress_rule.perforce_alb_outbound_to_p4_code_review](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_egress_rule.perforce_nlb_outbound_to_p4_broker](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | | [aws_vpc_security_group_egress_rule.perforce_nlb_outbound_to_perforce_web_services_alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | | [aws_vpc_security_group_ingress_rule.p4_auth_inbound_from_perforce_web_services_alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | +| [aws_vpc_security_group_ingress_rule.p4_broker_inbound_from_perforce_nlb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_vpc_security_group_ingress_rule.p4_code_review_inbound_from_perforce_web_services_alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | +| [aws_vpc_security_group_ingress_rule.p4_server_inbound_from_p4_broker](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_vpc_security_group_ingress_rule.p4_server_inbound_from_p4_code_review](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_vpc_security_group_ingress_rule.perforce_web_services_inbound_from_p4_server](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_vpc_security_group_ingress_rule.perforce_web_services_inbound_from_perforce_nlb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | @@ -218,9 +227,10 @@ packer build perforce_x86.pkr.hcl | [enable\_shared\_lb\_access\_logs](#input\_enable\_shared\_lb\_access\_logs) | Enables access logging for both the shared NLB and shared ALB. Defaults to false. | `bool` | `false` | no | | [existing\_ecs\_cluster\_name](#input\_existing\_ecs\_cluster\_name) | The name of an existing ECS cluster to use for the Perforce server. If omitted a new cluster will be created. | `string` | `null` | no | | [existing\_security\_groups](#input\_existing\_security\_groups) | A list of existing security group IDs to attach to the shared network load balancer. | `list(string)` | `[]` | no | -| [p4\_auth\_config](#input\_p4\_auth\_config) | # General
name: "The string including in the naming of resources related to P4Auth. Default is 'p4-auth'."

project\_prefix : "The project prefix for the P4Auth service. Default is 'cgd'."

environment : "The environment where the P4Auth service will be deployed. Default is 'dev'."

enable\_web\_based\_administration: "Whether to de enable web based administration. Default is 'true'."

debug : "Whether to enable debug mode for the P4Auth service. Default is 'false'."

fully\_qualified\_domain\_name : "The FQDN for the P4Auth Service. This is used for the P4Auth's Perforce configuration."


# Compute
cluster\_name : "The name of the ECS cluster where the P4Auth service will be deployed. Cluster is not created if this variable is null."

container\_name : "The name of the P4Auth service container. Default is 'p4-auth-container'."

container\_port : "The port on which the P4Auth service will be listening. Default is '3000'."

container\_cpu : "The number of CPU units to reserve for the P4Auth service container. Default is '1024'."

container\_memory : "The number of CPU units to reserve for the P4Auth service container. Default is '4096'."

pd4\_port : "The full URL you will use to access the P4 Depot in clients such P4V and P4Admin. Note, this typically starts with 'ssl:' and ends with the default port of ':1666'."


# Storage & Logging
cloudwatch\_log\_retention\_in\_days : "The number of days to retain the P4Auth service logs in CloudWatch. Default is 365 days."


# Networking
create\_defaults\_sgs : "Whether to create default security groups for the P4Auth service."

internal : "Set this flag to true if you do not want the P4Auth service to have a public IP."

create\_default\_role : "Whether to create the P4Auth default IAM Role. Default is set to true."

custom\_role : "ARN of a custom IAM Role you wish to use with P4Auth."

admin\_username\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4Auth Administrator username."

admin\_password\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4Auth Administrator password."


# - SCIM -
p4d\_super\_user\_arn : "If you would like to use SCIM to provision users and groups, you need to set this variable to the ARN of an AWS Secrets Manager secret containing the super user username for p4d."

p4d\_super\_user\_password\_arn : "If you would like to use SCIM to provision users and groups, you need to set this variable to the ARN of an AWS Secrets Manager secret containing the super user password for p4d."

scim\_bearer\_token\_arn : "If you would like to use SCIM to provision users and groups, you need to set this variable to the ARN of an AWS Secrets Manager secret containing the bearer token."

extra\_env : "Extra configuration environment variables to set on the p4 auth svc container." |
object({
# - General -
name = optional(string, "p4-auth")
project_prefix = optional(string, "cgd")
environment = optional(string, "dev")
enable_web_based_administration = optional(bool, true)
debug = optional(bool, false)
fully_qualified_domain_name = string

# - Compute -
container_name = optional(string, "p4-auth-container")
container_port = optional(number, 3000)
container_cpu = optional(number, 1024)
container_memory = optional(number, 4096)
p4d_port = optional(string, null)

# - Storage & Logging -
cloudwatch_log_retention_in_days = optional(number, 365)

# - Networking & Security -
service_subnets = optional(list(string), null)
create_default_sgs = optional(bool, true)
existing_security_groups = optional(list(string), [])
internal = optional(bool, false)

certificate_arn = optional(string, null)
create_default_role = optional(bool, true)
custom_role = optional(string, null)
admin_username_secret_arn = optional(string, null)
admin_password_secret_arn = optional(string, null)

# SCIM
p4d_super_user_arn = optional(string, null)
p4d_super_user_password_arn = optional(string, null)
scim_bearer_token_arn = optional(string, null)
extra_env = optional(map(string), null)
})
| `null` | no | -| [p4\_code\_review\_config](#input\_p4\_code\_review\_config) | # General
name: "The string including in the naming of resources related to P4 Code Review. Default is 'p4-code-review'."

project\_prefix : "The project prefix for the P4 Code Review service. Default is 'cgd'."

environment : "The environment where the P4 Code Review service will be deployed. Default is 'dev'."

debug : "Whether to enable debug mode for the P4 Code Review service. Default is 'false'."

fully\_qualified\_domain\_name : "The FQDN for the P4 Code Review Service. This is used for the P4 Code Review's Perforce configuration."


# Compute
container\_name : "The name of the P4 Code Review service container. Default is 'p4-code-review-container'."

container\_port : "The port on which the P4 Code Review service will be listening. Default is '3000'."

container\_cpu : "The number of CPU units to reserve for the P4 Code Review service container. Default is '1024'."

container\_memory : "The number of CPU units to reserve for the P4 Code Review service container. Default is '4096'."

pd4\_port : "The full URL you will use to access the P4 Depot in clients such P4V and P4Admin. Note, this typically starts with 'ssl:' and ends with the default port of ':1666'."

p4charset : "The P4CHARSET environment variable to set in the P4 Code Review container."

existing\_redis\_connection : "The existing Redis connection for the P4 Code Review service."


# Storage & Logging
cloudwatch\_log\_retention\_in\_days : "The number of days to retain the P4 Code Review service logs in CloudWatch. Default is 365 days."


# Networking & Security
create\_default\_sgs : "Whether to create default security groups for the P4 Code Review service."

internal : "Set this flag to true if you do not want the P4 Code Review service to have a public IP."

create\_default\_role : "Whether to create the P4 Code Review default IAM Role. Default is set to true."

custom\_role : "ARN of a custom IAM Role you wish to use with P4 Code Review."

super\_user\_password\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review Administrator username."

super\_user\_username\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review Administrator password."

p4d\_p4\_code\_review\_user\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review user's username."

p4d\_p4\_code\_review\_password\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review user's password."

p4d\_p4\_code\_review\_user\_password\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review user's password."

enable\_sso : "Whether to enable SSO for the P4 Code Review service. Default is set to false."

config\_php\_source : "Used as the ValueFrom for P4CR's config.php. Contents should be base64 encoded, and will be combined with the generated config.php via array\_replace\_recursive."


# Caching
elasticache\_node\_count : "The number of Elasticache nodes to create for the P4 Code Review service. Default is '1'."

elasticache\_node\_type : "The type of Elasticache node to create for the P4 Code Review service. Default is 'cache.t4g.micro'." |
object({
# General
name = optional(string, "p4-code-review")
project_prefix = optional(string, "cgd")
environment = optional(string, "dev")
debug = optional(bool, false)
fully_qualified_domain_name = string

# Compute
container_name = optional(string, "p4-code-review-container")
container_port = optional(number, 80)
container_cpu = optional(number, 1024)
container_memory = optional(number, 4096)
p4d_port = optional(string, null)
p4charset = optional(string, null)
existing_redis_connection = optional(object({
host = string
port = number
}), null)

# Storage & Logging
cloudwatch_log_retention_in_days = optional(number, 365)

# Networking & Security
create_default_sgs = optional(bool, true)
existing_security_groups = optional(list(string), [])
internal = optional(bool, false)
service_subnets = optional(list(string), null)

create_default_role = optional(bool, true)
custom_role = optional(string, null)

super_user_password_secret_arn = optional(string, null)
super_user_username_secret_arn = optional(string, null)
p4_code_review_user_password_secret_arn = optional(string, null)
p4_code_review_user_username_secret_arn = optional(string, null)
enable_sso = optional(string, false)
config_php_source = optional(string, null)

# Caching
elasticache_node_count = optional(number, 1)
elasticache_node_type = optional(string, "cache.t4g.micro")
})
| `null` | no | -| [p4\_server\_config](#input\_p4\_server\_config) | # - General -
name: "The string including in the naming of resources related to P4 Server. Default is 'p4-server'"

project\_prefix: "The project prefix for this workload. This is appended to the beginning of most resource names."

environment: "The current environment (e.g. dev, prod, etc.)"

auth\_service\_url: "The URL for the P4Auth Service."

fully\_qualified\_domain\_name = "The FQDN for the P4 Server. This is used for the P4 Server's Perforce configuration."


# - Compute -
lookup\_existing\_ami : "Whether to lookup the existing Perforce P4 Server AMI."

ami\_prefix: "The AMI prefix to use for the AMI that will be created for P4 Server."

instance\_type: "The instance type for Perforce P4 Server. Defaults to c6g.large."

instance\_architecture: "The architecture of the P4 Server instance. Allowed values are 'arm64' or 'x86\_64'."

IMPORTANT: "Ensure the instance family of the instance type you select supports the instance\_architecture you select. For example, 'c6in' instance family only works for 'x86\_64' architecture, not 'arm64'. For a full list of this mapping, see the AWS Docs for EC2 Naming Conventions: https://docs.aws.amazon.com/ec2/latest/instancetypes/instance-type-names.html"

p4\_server\_type: "The Perforce P4 Server server type. Valid values are 'p4d\_commit' or 'p4d\_replica'."

unicode: "Whether to enable Unicode configuration for P4 Server the -xi flag for p4d. Set to true to enable Unicode support."

selinux: "Whether to apply SELinux label updates for P4 Server. Don't enable this if SELinux is disabled on your target operating system."

case\_sensitive: "Whether or not the server should be case insensitive (Server will run '-C1' mode), or if the server will run with case sensitivity default of the underlying platform. False enables '-C1' mode. Default is set to true."

plaintext: "Whether to enable plaintext authentication for P4 Server. This is not recommended for production environments unless you are using a load balancer for TLS termination. Default is set to false."


# - Storage -
storage\_type: "The type of backing store. Valid values are either 'EBS' or 'FSxN'"

depot\_volume\_size: "The size of the depot volume in GiB. Defaults to 128 GiB."

metadata\_volume\_size: "The size of the metadata volume in GiB. Defaults to 32 GiB."

logs\_volume\_size: "The size of the logs volume in GiB. Defaults to 32 GiB."


# - Networking & Security -
instance\_subnet\_id: "The subnet where the P4 Server instance will be deployed."

instance\_private\_ip: "The private IP address to assign to the P4 Server."

create\_default\_sg : "Whether to create a default security group for the P4 Server instance."

existing\_security\_groups: "A list of existing security group IDs to attach to the P4 Server load balancer."

internal: "Set this flag to true if you do not want the P4 Server instance to have a public IP."

super\_user\_password\_secret\_arn: "If you would like to manage your own super user credentials through AWS Secrets Manager provide the ARN for the super user's username here. Otherwise, the default of 'perforce' will be used."

super\_user\_username\_secret\_arn: "If you would like to manage your own super user credentials through AWS Secrets Manager provide the ARN for the super user's password here."

create\_default\_role: "Optional creation of P4 Server default IAM Role with SSM managed instance core policy attached. Default is set to true."

custom\_role: "ARN of a custom IAM Role you wish to use with P4 Server." |
object({
# General
name = optional(string, "p4-server")
project_prefix = optional(string, "cgd")
environment = optional(string, "dev")
auth_service_url = optional(string, null)
fully_qualified_domain_name = string

# Compute
lookup_existing_ami = optional(bool, true)
ami_prefix = optional(string, "p4_al2023")

instance_type = optional(string, "c6i.large")
instance_architecture = optional(string, "x86_64")
p4_server_type = optional(string, null)

unicode = optional(bool, false)
selinux = optional(bool, false)
case_sensitive = optional(bool, true)
plaintext = optional(bool, false)

# Storage
storage_type = optional(string, "EBS")
depot_volume_size = optional(number, 128)
metadata_volume_size = optional(number, 32)
logs_volume_size = optional(number, 32)

# Networking & Security
instance_subnet_id = optional(string, null)
instance_private_ip = optional(string, null)
create_default_sg = optional(bool, true)
existing_security_groups = optional(list(string), [])
internal = optional(bool, false)

super_user_password_secret_arn = optional(string, null)
super_user_username_secret_arn = optional(string, null)

create_default_role = optional(bool, true)
custom_role = optional(string, null)

# FSxN
fsxn_password = optional(string, null)
fsxn_filesystem_security_group_id = optional(string, null)
protocol = optional(string, null)
fsxn_region = optional(string, null)
fsxn_management_ip = optional(string, null)
fsxn_svm_name = optional(string, null)
amazon_fsxn_svm_id = optional(string, null)
fsxn_aws_profile = optional(string, null)
})
| `null` | no | +| [p4\_auth\_config](#input\_p4\_auth\_config) | # General
name: "The string including in the naming of resources related to P4Auth. Default is 'p4-auth'."

project\_prefix : "The project prefix for the P4Auth service. Default is 'cgd'."

environment : "The environment where the P4Auth service will be deployed. Default is 'dev'."

enable\_web\_based\_administration: "Whether to de enable web based administration. Default is 'true'."

debug : "Whether to enable debug mode for the P4Auth service. Default is 'false'."

fully\_qualified\_domain\_name : "The FQDN for the P4Auth Service. This is used for the P4Auth's Perforce configuration."


# Compute
cluster\_name : "The name of the ECS cluster where the P4Auth service will be deployed. Cluster is not created if this variable is null."

container\_name : "The name of the P4Auth service container. Default is 'p4-auth-container'."

container\_port : "The port on which the P4Auth service will be listening. Default is '3000'."

container\_cpu : "The number of CPU units to reserve for the P4Auth service container. Default is '1024'."

container\_memory : "The number of CPU units to reserve for the P4Auth service container. Default is '4096'."

pd4\_port : "The full URL you will use to access the P4 Depot in clients such P4V and P4Admin. Note, this typically starts with 'ssl:' and ends with the default port of ':1666'."


# Storage & Logging
cloudwatch\_log\_retention\_in\_days : "The number of days to retain the P4Auth service logs in CloudWatch. Default is 365 days."


# Networking
create\_defaults\_sgs : "Whether to create default security groups for the P4Auth service."

internal : "Set this flag to true if you do not want the P4Auth service to have a public IP."

create\_default\_role : "Whether to create the P4Auth default IAM Role. Default is set to true."

custom\_role : "ARN of a custom IAM Role you wish to use with P4Auth."

admin\_username\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4Auth Administrator username."

admin\_password\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4Auth Administrator password."


# - SCIM -
p4d\_super\_user\_arn : "If you would like to use SCIM to provision users and groups, you need to set this variable to the ARN of an AWS Secrets Manager secret containing the super user username for p4d."

p4d\_super\_user\_password\_arn : "If you would like to use SCIM to provision users and groups, you need to set this variable to the ARN of an AWS Secrets Manager secret containing the super user password for p4d."

scim\_bearer\_token\_arn : "If you would like to use SCIM to provision users and groups, you need to set this variable to the ARN of an AWS Secrets Manager secret containing the bearer token."

extra\_env : "Extra configuration environment variables to set on the p4 auth svc container." |
object({
# - General -
name = optional(string, "p4-auth")
project_prefix = optional(string, "cgd")
environment = optional(string, "dev")
enable_web_based_administration = optional(bool, true)
debug = optional(bool, false)
fully_qualified_domain_name = string

# - Compute -
container_name = optional(string, "p4-auth-container")
container_port = optional(number, 3000)
container_cpu = optional(number, 1024)
container_memory = optional(number, 4096)
p4d_port = optional(string, null)

# - Storage & Logging -
cloudwatch_log_retention_in_days = optional(number, 365)

# - Networking & Security -
service_subnets = optional(list(string), null)
create_default_sgs = optional(bool, true)
existing_security_groups = optional(list(string), [])
internal = optional(bool, false)

certificate_arn = optional(string, null)
create_default_role = optional(bool, true)
custom_role = optional(string, null)
admin_username_secret_arn = optional(string, null)
admin_password_secret_arn = optional(string, null)

# SCIM
p4d_super_user_arn = optional(string, null)
p4d_super_user_password_arn = optional(string, null)
scim_bearer_token_arn = optional(string, null)
extra_env = optional(map(string), null)
})
| `null` | no | +| [p4\_broker\_config](#input\_p4\_broker\_config) | # General
name: "The string included in the naming of resources related to P4 Broker. Default is 'p4-broker'."

project\_prefix : "The project prefix for the P4 Broker service. Default is 'cgd'."

debug : "Whether to enable debug mode for the P4 Broker service. Default is 'false'."


# Compute
container\_name : "The name of the P4 Broker service container. Default is 'p4-broker-container'."

container\_port : "The port on which the P4 Broker service will be listening. Default is '1666'."

container\_cpu : "The number of CPU units to reserve for the P4 Broker service container. Default is '1024'."

container\_memory : "The amount of memory in MiB to reserve for the P4 Broker service container. Default is '2048'."

container\_image : "The Docker image URI for the P4 Broker container. Required."

desired\_count : "The desired number of P4 Broker ECS tasks. Default is '1'."


# Broker Configuration
p4\_target : "The upstream Perforce server target (e.g., ssl:p4server:1666). Required."

broker\_command\_rules : "Command filtering rules for the P4 Broker configuration. Default passes all commands."

extra\_env : "Extra environment variables to set on the P4 Broker container."


# Storage & Logging
cloudwatch\_log\_retention\_in\_days : "The number of days to retain the P4 Broker service logs in CloudWatch. Default is 365 days."


# Networking & Security
service\_subnets : "A list of subnets to deploy the P4 Broker ECS Service into."

create\_default\_role : "Whether to create the P4 Broker default IAM Role. Default is set to true."

custom\_role : "ARN of a custom IAM Role you wish to use with P4 Broker." |
object({
# General
name = optional(string, "p4-broker")
project_prefix = optional(string, "cgd")
debug = optional(bool, false)

# Compute
container_name = optional(string, "p4-broker-container")
container_port = optional(number, 1666)
container_cpu = optional(number, 1024)
container_memory = optional(number, 2048)
container_image = string
desired_count = optional(number, 1)

# Broker Configuration
p4_target = string
broker_command_rules = optional(list(object({
command = string
action = string
message = optional(string, null)
})), [{ command = "*", action = "pass", message = null }])
extra_env = optional(map(string), null)

# Storage & Logging
cloudwatch_log_retention_in_days = optional(number, 365)

# Networking & Security
service_subnets = optional(list(string), null)
create_default_role = optional(bool, true)
custom_role = optional(string, null)
})
| `null` | no | +| [p4\_code\_review\_config](#input\_p4\_code\_review\_config) | # General
name: "The string including in the naming of resources related to P4 Code Review. Default is 'p4-code-review'."

project\_prefix : "The project prefix for the P4 Code Review service. Default is 'cgd'."

environment : "The environment where the P4 Code Review service will be deployed. Default is 'dev'."

debug : "Whether to enable debug mode for the P4 Code Review service. Default is 'false'."

fully\_qualified\_domain\_name : "The FQDN for the P4 Code Review Service. This is used for the P4 Code Review's Perforce configuration."


# Compute
container\_name : "The name of the P4 Code Review service container. Default is 'p4-code-review-container'."

container\_port : "The port on which the P4 Code Review service will be listening. Default is '3000'."

container\_cpu : "The number of CPU units to reserve for the P4 Code Review service container. Default is '1024'."

container\_memory : "The number of CPU units to reserve for the P4 Code Review service container. Default is '4096'."

pd4\_port : "The full URL you will use to access the P4 Depot in clients such P4V and P4Admin. Note, this typically starts with 'ssl:' and ends with the default port of ':1666'."

p4charset : "The P4CHARSET environment variable to set in the P4 Code Review container."

existing\_redis\_connection : "The existing Redis connection for the P4 Code Review service."


# Storage & Logging
cloudwatch\_log\_retention\_in\_days : "The number of days to retain the P4 Code Review service logs in CloudWatch. Default is 365 days."


# Networking & Security
create\_default\_sgs : "Whether to create default security groups for the P4 Code Review service."

internal : "Set this flag to true if you do not want the P4 Code Review service to have a public IP."

create\_default\_role : "Whether to create the P4 Code Review default IAM Role. Default is set to true."

custom\_role : "ARN of a custom IAM Role you wish to use with P4 Code Review."

super\_user\_password\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review Administrator username."

super\_user\_username\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review Administrator password."

p4d\_p4\_code\_review\_user\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review user's username."

p4d\_p4\_code\_review\_password\_secret\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review user's password."

p4d\_p4\_code\_review\_user\_password\_arn : "Optionally provide the ARN of an AWS Secret for the P4 Code Review user's password."

enable\_sso : "Whether to enable SSO for the P4 Code Review service. Default is set to false."

config\_php\_source : "Used as the ValueFrom for P4CR's config.php. Contents should be base64 encoded, and will be combined with the generated config.php via array\_replace\_recursive."


# Caching
elasticache\_node\_count : "The number of Elasticache nodes to create for the P4 Code Review service. Default is '1'."

elasticache\_node\_type : "The type of Elasticache node to create for the P4 Code Review service. Default is 'cache.t4g.micro'." |
object({
# General
name = optional(string, "p4-code-review")
project_prefix = optional(string, "cgd")
environment = optional(string, "dev")
debug = optional(bool, false)
fully_qualified_domain_name = string

# Compute
container_name = optional(string, "p4-code-review-container")
container_port = optional(number, 80)
container_cpu = optional(number, 1024)
container_memory = optional(number, 4096)
p4d_port = optional(string, null)
p4charset = optional(string, null)
existing_redis_connection = optional(object({
host = string
port = number
}), null)

# Storage & Logging
cloudwatch_log_retention_in_days = optional(number, 365)

# Networking & Security
create_default_sgs = optional(bool, true)
existing_security_groups = optional(list(string), [])
internal = optional(bool, false)
service_subnets = optional(list(string), null)

create_default_role = optional(bool, true)
custom_role = optional(string, null)

super_user_password_secret_arn = optional(string, null)
super_user_username_secret_arn = optional(string, null)
p4_code_review_user_password_secret_arn = optional(string, null)
p4_code_review_user_username_secret_arn = optional(string, null)
enable_sso = optional(string, true)
config_php_source = optional(string, null)

# Caching
elasticache_node_count = optional(number, 1)
elasticache_node_type = optional(string, "cache.t4g.micro")
})
| `null` | no | +| [p4\_server\_config](#input\_p4\_server\_config) | # - General -
name: "The string including in the naming of resources related to P4 Server. Default is 'p4-server'"

project\_prefix: "The project prefix for this workload. This is appended to the beginning of most resource names."

environment: "The current environment (e.g. dev, prod, etc.)"

auth\_service\_url: "The URL for the P4Auth Service."

fully\_qualified\_domain\_name = "The FQDN for the P4 Server. This is used for the P4 Server's Perforce configuration."


# - Compute -
lookup\_existing\_ami : "Whether to lookup the existing Perforce P4 Server AMI."

ami\_prefix: "The AMI prefix to use for the AMI that will be created for P4 Server."

instance\_type: "The instance type for Perforce P4 Server. Defaults to c6g.large."

instance\_architecture: "The architecture of the P4 Server instance. Allowed values are 'arm64' or 'x86\_64'."

IMPORTANT: "Ensure the instance family of the instance type you select supports the instance\_architecture you select. For example, 'c6in' instance family only works for 'x86\_64' architecture, not 'arm64'. For a full list of this mapping, see the AWS Docs for EC2 Naming Conventions: https://docs.aws.amazon.com/ec2/latest/instancetypes/instance-type-names.html"

p4\_server\_type: "The Perforce P4 Server server type. Valid values are 'p4d\_commit' or 'p4d\_replica'."

unicode: "Whether to enable Unicode configuration for P4 Server the -xi flag for p4d. Set to true to enable Unicode support."

selinux: "Whether to apply SELinux label updates for P4 Server. Don't enable this if SELinux is disabled on your target operating system."

case\_sensitive: "Whether or not the server should be case insensitive (Server will run '-C1' mode), or if the server will run with case sensitivity default of the underlying platform. False enables '-C1' mode. Default is set to true."

plaintext: "Whether to enable plaintext authentication for P4 Server. This is not recommended for production environments unless you are using a load balancer for TLS termination. Default is set to false."


# - Storage -
storage\_type: "The type of backing store. Valid values are either 'EBS' or 'FSxN'"

depot\_volume\_size: "The size of the depot volume in GiB. Defaults to 128 GiB."

metadata\_volume\_size: "The size of the metadata volume in GiB. Defaults to 32 GiB."

logs\_volume\_size: "The size of the logs volume in GiB. Defaults to 32 GiB."


# - Networking & Security -
instance\_subnet\_id: "The subnet where the P4 Server instance will be deployed."

instance\_private\_ip: "The private IP address to assign to the P4 Server."

create\_default\_sg : "Whether to create a default security group for the P4 Server instance."

existing\_security\_groups: "A list of existing security group IDs to attach to the P4 Server load balancer."

internal: "Set this flag to true if you do not want the P4 Server instance to have a public IP."

super\_user\_password\_secret\_arn: "If you would like to manage your own super user credentials through AWS Secrets Manager provide the ARN for the super user's username here. Otherwise, the default of 'perforce' will be used."

super\_user\_username\_secret\_arn: "If you would like to manage your own super user credentials through AWS Secrets Manager provide the ARN for the super user's password here."

create\_default\_role: "Optional creation of P4 Server default IAM Role with SSM managed instance core policy attached. Default is set to true."

custom\_role: "ARN of a custom IAM Role you wish to use with P4 Server." |
object({
# General
name = optional(string, "p4-server")
project_prefix = optional(string, "cgd")
environment = optional(string, "dev")
auth_service_url = optional(string, null)
fully_qualified_domain_name = string

# Compute
lookup_existing_ami = optional(bool, true)
ami_prefix = optional(string, "p4_al2023")

instance_type = optional(string, "c6i.large")
instance_architecture = optional(string, "x86_64")
p4_server_type = optional(string, null)

unicode = optional(bool, false)
selinux = optional(bool, false)
case_sensitive = optional(bool, true)
plaintext = optional(bool, false)

# Storage
storage_type = optional(string, "EBS")
depot_volume_size = optional(number, 128)
metadata_volume_size = optional(number, 32)
logs_volume_size = optional(number, 32)

# Networking & Security
instance_subnet_id = optional(string, null)
instance_private_ip = optional(string, null)
create_default_sg = optional(bool, true)
existing_security_groups = optional(list(string), [])
internal = optional(bool, false)

super_user_password_secret_arn = optional(string, null)
super_user_username_secret_arn = optional(string, null)

create_default_role = optional(bool, true)
custom_role = optional(string, null)

# FSxN
fsxn_password = optional(string, null)
fsxn_filesystem_security_group_id = optional(string, null)
protocol = optional(string, null)
fsxn_region = optional(string, null)
fsxn_management_ip = optional(string, null)
fsxn_svm_name = optional(string, null)
amazon_fsxn_svm_id = optional(string, null)
fsxn_aws_profile = optional(string, null)
})
| `null` | no | | [project\_prefix](#input\_project\_prefix) | The project prefix for this workload. This is appended to the beginning of most resource names. | `string` | `"cgd"` | no | | [route53\_private\_hosted\_zone\_name](#input\_route53\_private\_hosted\_zone\_name) | The name of the private Route53 Hosted Zone for the Perforce resources. | `string` | `null` | no | | [s3\_enable\_force\_destroy](#input\_s3\_enable\_force\_destroy) | Enables force destroy for the S3 bucket for both the shared NLB and shared ALB access log storage. Defaults to true. | `bool` | `true` | no | @@ -232,7 +242,7 @@ packer build perforce_x86.pkr.hcl | [shared\_network\_load\_balancer\_name](#input\_shared\_network\_load\_balancer\_name) | The name of the shared Network Load Balancer for the Perforce resources. | `string` | `"p4nlb"` | no | | [shared\_nlb\_access\_logs\_prefix](#input\_shared\_nlb\_access\_logs\_prefix) | Log prefix for shared NLB access logs. | `string` | `"perforce-nlb-"` | no | | [shared\_nlb\_subnets](#input\_shared\_nlb\_subnets) | A list of subnets to attach to the shared network load balancer. | `list(string)` | `null` | no | -| [tags](#input\_tags) | Tags to apply to resources. | `map(any)` |
{
"IaC": "Terraform",
"ModuleBy": "CGD-Toolkit",
"ModuleName": "terraform-aws-perforce",
"ModuleSource": "https://github.com/aws-games/cloud-game-development-toolkit/tree/main/modules/perforce",
"RootModuleName": "-"
}
| no | +| [tags](#input\_tags) | Tags to apply to resources. | `map(any)` |
{
"IaC": "Terraform",
"ModuleBy": "CGD-Toolkit",
"ModuleName": "terraform-aws-perforce",
"ModuleSource": "https://github.com/aws-games/cloud-game-development-toolkit/tree/main/modules/perforce",
"RootModuleName": "-"
}
| no | ## Outputs @@ -244,6 +254,10 @@ packer build perforce_x86.pkr.hcl | [p4\_auth\_perforce\_cluster\_name](#output\_p4\_auth\_perforce\_cluster\_name) | Name of the ECS cluster hosting P4Auth. | | [p4\_auth\_service\_security\_group\_id](#output\_p4\_auth\_service\_security\_group\_id) | Security group associated with the ECS service running P4Auth. | | [p4\_auth\_target\_group\_arn](#output\_p4\_auth\_target\_group\_arn) | The service target group for the P4Auth. | +| [p4\_broker\_cluster\_name](#output\_p4\_broker\_cluster\_name) | Name of the ECS cluster hosting P4 Broker. | +| [p4\_broker\_config\_bucket\_name](#output\_p4\_broker\_config\_bucket\_name) | The name of the S3 bucket containing the P4 Broker configuration. | +| [p4\_broker\_service\_security\_group\_id](#output\_p4\_broker\_service\_security\_group\_id) | Security group associated with the ECS service running P4 Broker. | +| [p4\_broker\_target\_group\_arn](#output\_p4\_broker\_target\_group\_arn) | The NLB target group ARN for P4 Broker. | | [p4\_code\_review\_alb\_dns\_name](#output\_p4\_code\_review\_alb\_dns\_name) | The DNS name of the P4 Code Review ALB. | | [p4\_code\_review\_alb\_security\_group\_id](#output\_p4\_code\_review\_alb\_security\_group\_id) | Security group associated with the P4 Code Review load balancer. | | [p4\_code\_review\_alb\_zone\_id](#output\_p4\_code\_review\_alb\_zone\_id) | The hosted zone ID of the P4 Code Review ALB. | diff --git a/modules/perforce/lb.tf b/modules/perforce/lb.tf index 0fc5a42f..2505c55b 100644 --- a/modules/perforce/lb.tf +++ b/modules/perforce/lb.tf @@ -4,7 +4,7 @@ ########################################## # Send traffic from NLB to ALB resource "aws_lb_target_group" "perforce" { - count = var.create_shared_network_load_balancer != false ? 1 : 0 + count = var.create_shared_network_load_balancer && var.create_shared_application_load_balancer ? 1 : 0 name = "${var.project_prefix}-nlb-to-perforce-web-services" target_type = "alb" port = 443 @@ -40,7 +40,7 @@ resource "aws_lb_target_group" "perforce" { } resource "aws_lb_target_group_attachment" "perforce" { - count = var.create_shared_network_load_balancer != false ? 1 : 0 + count = var.create_shared_network_load_balancer && var.create_shared_application_load_balancer ? 1 : 0 target_group_arn = aws_lb_target_group.perforce[0].arn target_id = aws_lb.perforce_web_services[0].arn port = 443 @@ -97,7 +97,7 @@ resource "aws_lb" "perforce" { ########################################## # forward HTTPS traffic from Public NLB to Internal ALB resource "aws_lb_listener" "perforce" { - count = var.create_shared_network_load_balancer != false ? 1 : 0 + count = var.create_shared_network_load_balancer && var.create_shared_application_load_balancer ? 1 : 0 load_balancer_arn = aws_lb.perforce[0].arn port = 443 protocol = "TCP" @@ -129,6 +129,29 @@ resource "aws_lb_listener" "perforce" { } +########################################## +# Perforce NLB | P4 Broker TCP Listener +########################################## +# Forward TCP traffic on broker port from NLB to P4 Broker target group +resource "aws_lb_listener" "perforce_broker" { + count = var.p4_broker_config != null && var.create_shared_network_load_balancer ? 1 : 0 + load_balancer_arn = aws_lb.perforce[0].arn + port = var.p4_broker_config.container_port + protocol = "TCP" + + default_action { + type = "forward" + target_group_arn = module.p4_broker[0].target_group_arn + } + + #checkov:skip=CKV2_AWS_74: TCP listener does not support TLS ciphers + tags = merge(var.tags, { + TrafficSource = (var.shared_network_load_balancer_name != null ? var.shared_network_load_balancer_name : "${var.project_prefix}-perforce-shared-nlb") + TrafficDestination = "${var.project_prefix}-${var.p4_broker_config.name}-service" + }) +} + + # 1. Create the Target Group (this is done in p4-auth, and p4-code-review submodules) # 2. Create the Target Group Attachment (this is not necessary as ECS handles this automatically. This is handled in the p4-auth, and p4-code-review submodules in the load_balancers block) # 3. Create the ALB only if the target group (in submodules) has been created diff --git a/modules/perforce/locals.tf b/modules/perforce/locals.tf index b513a204..7281f197 100644 --- a/modules/perforce/locals.tf +++ b/modules/perforce/locals.tf @@ -1,7 +1,7 @@ locals { # shared ECS cluster configuration create_shared_ecs_cluster = (var.existing_ecs_cluster_name == null && - (var.p4_auth_config != null || var.p4_code_review_config != null)) + (var.p4_auth_config != null || var.p4_code_review_config != null || var.p4_broker_config != null)) # This serves as a sensible default for p4d_port config options p4_port = var.p4_server_config != null ? ( diff --git a/modules/perforce/main.tf b/modules/perforce/main.tf index f499c696..cd336166 100644 --- a/modules/perforce/main.tf +++ b/modules/perforce/main.tf @@ -159,10 +159,10 @@ module "p4_code_review" { create_default_role = var.p4_code_review_config.create_default_role custom_role = var.p4_code_review_config.custom_role - super_user_password_secret_arn = module.p4_server[0].super_user_password_secret_arn - super_user_username_secret_arn = module.p4_server[0].super_user_username_secret_arn - p4_code_review_user_password_secret_arn = module.p4_server[0].super_user_password_secret_arn - p4_code_review_user_username_secret_arn = module.p4_server[0].super_user_username_secret_arn + super_user_password_secret_arn = var.p4_code_review_config.super_user_password_secret_arn != null ? var.p4_code_review_config.super_user_password_secret_arn : try(module.p4_server[0].super_user_password_secret_arn, null) + super_user_username_secret_arn = var.p4_code_review_config.super_user_username_secret_arn != null ? var.p4_code_review_config.super_user_username_secret_arn : try(module.p4_server[0].super_user_username_secret_arn, null) + p4_code_review_user_password_secret_arn = var.p4_code_review_config.p4_code_review_user_password_secret_arn != null ? var.p4_code_review_config.p4_code_review_user_password_secret_arn : try(module.p4_server[0].super_user_password_secret_arn, null) + p4_code_review_user_username_secret_arn = var.p4_code_review_config.p4_code_review_user_username_secret_arn != null ? var.p4_code_review_config.p4_code_review_user_username_secret_arn : try(module.p4_server[0].super_user_username_secret_arn, null) enable_sso = var.p4_code_review_config.enable_sso config_php_source = var.p4_code_review_config.config_php_source @@ -170,6 +170,50 @@ module "p4_code_review" { depends_on = [aws_ecs_cluster.perforce_web_services_cluster[0]] } +################################################# +# P4 Broker (Perforce Helix Broker) +################################################# +module "p4_broker" { + source = "./modules/p4-broker" + count = var.p4_broker_config != null ? 1 : 0 + + # General + name = var.p4_broker_config.name + project_prefix = var.p4_broker_config.project_prefix + debug = var.p4_broker_config.debug + + # Compute + cluster_name = ( + var.existing_ecs_cluster_name != null ? + var.existing_ecs_cluster_name : + aws_ecs_cluster.perforce_web_services_cluster[0].name + ) + container_name = var.p4_broker_config.container_name + container_port = var.p4_broker_config.container_port + container_cpu = var.p4_broker_config.container_cpu + container_memory = var.p4_broker_config.container_memory + container_image = var.p4_broker_config.container_image + desired_count = var.p4_broker_config.desired_count + + # Broker Configuration + p4_target = var.p4_broker_config.p4_target + broker_command_rules = var.p4_broker_config.broker_command_rules + extra_env = var.p4_broker_config.extra_env + + # Storage & Logging + cloudwatch_log_retention_in_days = var.p4_broker_config.cloudwatch_log_retention_in_days + + # Networking & Security + vpc_id = var.vpc_id + subnets = var.p4_broker_config.service_subnets + + create_default_role = var.p4_broker_config.create_default_role + custom_role = var.p4_broker_config.custom_role + + depends_on = [aws_ecs_cluster.perforce_web_services_cluster[0]] +} + + ################################################# # Shared ECS Cluster (Perforce Web Services) ################################################# diff --git a/modules/perforce/modules/p4-broker/README.md b/modules/perforce/modules/p4-broker/README.md new file mode 100644 index 00000000..10ade281 --- /dev/null +++ b/modules/perforce/modules/p4-broker/README.md @@ -0,0 +1,226 @@ +# P4 Broker (Perforce Helix Broker) + +This module deploys a Perforce Helix Broker (`p4broker`) as an ECS Fargate service. P4 Broker is a TCP-level proxy/filter that sits between Perforce clients and the Perforce server. It uses a broker configuration file to define routing rules, command filtering, and redirection. + +## Architecture + +P4 Broker operates at the Perforce protocol level and requires a TCP listener on the shared Network Load Balancer (NLB). Unlike P4 Auth and P4 Code Review (which are HTTP services behind the shared ALB), P4 Broker handles raw TCP Perforce protocol traffic. + +```text +Client --> NLB (TCP:1666) --> P4 Broker (ECS) --> P4 Server (EC2) +``` + +## Usage + +```hcl +module "p4_broker" { + source = "./modules/p4-broker" + + # General + name = "p4-broker" + project_prefix = "cgd" + + # Compute + cluster_name = "my-ecs-cluster" + container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/p4-broker:latest" + + # Broker Configuration + p4_target = "ssl:p4server:1666" + broker_command_rules = [ + { + command = "*" + action = "pass" + } + ] + + # Networking + vpc_id = "vpc-12345678" + subnets = ["subnet-111", "subnet-222"] +} +``` + +## Broker Configuration + +The broker configuration file (`p4broker.conf`) is generated from Terraform variables and uploaded to S3. An init container downloads the configuration before the broker starts. + +### Command Rules + +Command rules control how `p4broker` handles client commands: + +```hcl +broker_command_rules = [ + { + command = "obliterate" + action = "reject" + message = "Obliterate is not permitted through the broker." + }, + { + command = "*" + action = "pass" + } +] +``` + +## Container Image + +A Dockerfile for building the P4 Broker image is provided in `assets/docker/perforce/p4-broker/`. The module accepts any container image URI via the `container_image` variable. + + +## Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.0 | +| aws | ~> 6.6 | +| random | ~> 3.7 | + +## Providers + +| Name | Version | +|------|---------| +| aws | ~> 6.6 | +| random | ~> 3.7 | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_ecs_cluster.cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | +| [aws_ecs_cluster_capacity_providers.cluster_fargate_providers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster_capacity_providers) | resource | +| [aws_ecs_service.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | +| [aws_ecs_task_definition.task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_iam_policy.default_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.s3_config_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.default_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.task_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_lb_target_group.nlb_target_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group) | resource | +| [aws_s3_bucket.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_object.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | resource | +| [aws_security_group.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| container\_image | The Docker image URI for the P4 Broker container. | `string` | n/a | yes | +| p4\_target | The upstream Perforce server target (e.g., ssl:p4server:1666). | `string` | n/a | yes | +| subnets | A list of subnets to deploy the P4 Broker ECS Service into. | `list(string)` | n/a | yes | +| vpc\_id | The ID of the existing VPC you would like to deploy P4 Broker into. | `string` | n/a | yes | +| broker\_command\_rules | Command filtering rules for the P4 Broker configuration. | `list(object)` | `[{command="*", action="pass"}]` | no | +| cloudwatch\_log\_retention\_in\_days | The log retention in days of the CloudWatch log group. | `number` | `365` | no | +| cluster\_name | The name of the ECS cluster to deploy into. | `string` | `null` | no | +| container\_cpu | The CPU allotment for the P4 Broker container. | `number` | `1024` | no | +| container\_memory | The memory allotment for the P4 Broker container. | `number` | `2048` | no | +| container\_name | The name of the P4 Broker container. | `string` | `"p4-broker-container"` | no | +| container\_port | The container port that P4 Broker listens on. | `number` | `1666` | no | +| create\_default\_role | Optional creation of P4 Broker default IAM Role. | `bool` | `true` | no | +| custom\_role | ARN of the custom IAM Role you wish to use with P4 Broker. | `string` | `null` | no | +| debug | Set this flag to enable execute command on service containers. | `bool` | `false` | no | +| desired\_count | The desired number of P4 Broker ECS tasks. | `number` | `1` | no | +| extra\_env | Extra environment variables to set on the P4 Broker container. | `map(string)` | `null` | no | +| name | The name attached to P4 Broker module resources. | `string` | `"p4-broker"` | no | +| project\_prefix | The project prefix for this workload. | `string` | `"cgd"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| cluster\_name | Name of the ECS cluster hosting P4 Broker | +| config\_bucket\_name | The name of the S3 bucket containing the broker configuration | +| service\_arn | The ARN of the P4 Broker ECS service | +| service\_security\_group\_id | Security group associated with the ECS service running P4 Broker | +| target\_group\_arn | The NLB target group ARN for P4 Broker | +| task\_definition\_arn | The ARN of the P4 Broker task definition | + + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | ~> 6.6 | +| [random](#requirement\_random) | ~> 3.7 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 6.6 | +| [random](#provider\_random) | ~> 3.7 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_ecs_cluster.cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | +| [aws_ecs_cluster_capacity_providers.cluster_fargate_providers](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster_capacity_providers) | resource | +| [aws_ecs_service.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | +| [aws_ecs_task_definition.task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_iam_policy.default_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.s3_config_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.default_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.task_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.default_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.task_execution_role_ecs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.task_execution_role_s3_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lb_target_group.nlb_target_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group) | resource | +| [aws_s3_bucket.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_public_access_block.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_versioning.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | +| [aws_s3_object.broker_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | resource | +| [aws_security_group.ecs_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_vpc_security_group_egress_rule.ecs_service_outbound_to_internet_ipv4](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_egress_rule.ecs_service_outbound_to_internet_ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [random_string.broker_config](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [random_string.p4_broker](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [aws_ecs_cluster.cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_cluster) | data source | +| [aws_iam_policy_document.default_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ecs_tasks_trust_relationship](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.s3_config_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [container\_image](#input\_container\_image) | The Docker image URI for the P4 Broker container. | `string` | n/a | yes | +| [p4\_target](#input\_p4\_target) | The upstream Perforce server target (e.g., ssl:p4server:1666). | `string` | n/a | yes | +| [subnets](#input\_subnets) | A list of subnets to deploy the P4 Broker ECS Service into. Private subnets are recommended. | `list(string)` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | The ID of the existing VPC you would like to deploy P4 Broker into. | `string` | n/a | yes | +| [broker\_command\_rules](#input\_broker\_command\_rules) | Command filtering rules for the P4 Broker configuration. |
list(object({
command = string
action = string
message = optional(string, null)
}))
|
[
{
"action": "pass",
"command": "*",
"message": null
}
]
| no | +| [cloudwatch\_log\_retention\_in\_days](#input\_cloudwatch\_log\_retention\_in\_days) | The log retention in days of the CloudWatch log group for P4 Broker. | `number` | `365` | no | +| [cluster\_name](#input\_cluster\_name) | The name of the ECS cluster to deploy the P4 Broker into. Cluster is not created if this variable is provided. | `string` | `null` | no | +| [container\_cpu](#input\_container\_cpu) | The CPU allotment for the P4 Broker container. | `number` | `1024` | no | +| [container\_memory](#input\_container\_memory) | The memory allotment for the P4 Broker container. | `number` | `2048` | no | +| [container\_name](#input\_container\_name) | The name of the P4 Broker container. | `string` | `"p4-broker-container"` | no | +| [container\_port](#input\_container\_port) | The container port that P4 Broker listens on. | `number` | `1666` | no | +| [create\_default\_role](#input\_create\_default\_role) | Optional creation of P4 Broker default IAM Role. Default is set to true. | `bool` | `true` | no | +| [custom\_role](#input\_custom\_role) | ARN of the custom IAM Role you wish to use with P4 Broker. | `string` | `null` | no | +| [debug](#input\_debug) | Set this flag to enable execute command on service containers and force redeploys. | `bool` | `false` | no | +| [desired\_count](#input\_desired\_count) | The desired number of P4 Broker ECS tasks. | `number` | `1` | no | +| [extra\_env](#input\_extra\_env) | Extra environment variables to set on the P4 Broker container. | `map(string)` | `null` | no | +| [name](#input\_name) | The name attached to P4 Broker module resources. | `string` | `"p4-broker"` | no | +| [project\_prefix](#input\_project\_prefix) | The project prefix for this workload. This is appended to the beginning of most resource names. | `string` | `"cgd"` | no | +| [tags](#input\_tags) | Tags to apply to resources. | `map(any)` |
{
"IaC": "Terraform",
"ModuleBy": "CGD-Toolkit",
"ModuleName": "p4-broker",
"ModuleSource": "https://github.com/aws-games/cloud-game-development-toolkit/tree/main/modules/perforce",
"RootModuleName": "terraform-aws-perforce"
}
| no | + +## Outputs + +| Name | Description | +|------|-------------| +| [cluster\_name](#output\_cluster\_name) | Name of the ECS cluster hosting P4 Broker | +| [config\_bucket\_name](#output\_config\_bucket\_name) | The name of the S3 bucket containing the broker configuration | +| [service\_arn](#output\_service\_arn) | The ARN of the P4 Broker ECS service | +| [service\_security\_group\_id](#output\_service\_security\_group\_id) | Security group associated with the ECS service running P4 Broker | +| [target\_group\_arn](#output\_target\_group\_arn) | The NLB target group ARN for P4 Broker | +| [task\_definition\_arn](#output\_task\_definition\_arn) | The ARN of the P4 Broker task definition | + + diff --git a/modules/perforce/modules/p4-broker/config.tf b/modules/perforce/modules/p4-broker/config.tf new file mode 100644 index 00000000..136f9573 --- /dev/null +++ b/modules/perforce/modules/p4-broker/config.tf @@ -0,0 +1,67 @@ +########################################## +# S3 | Broker Config Bucket +########################################## +resource "random_string" "broker_config" { + length = 8 + special = false + upper = false +} + +resource "aws_s3_bucket" "broker_config" { + bucket = "${local.name_prefix}-config-${random_string.broker_config.result}" + force_destroy = true + + #checkov:skip=CKV_AWS_21: Versioning not required for broker config + #checkov:skip=CKV_AWS_144: Cross-region replication not required + #checkov:skip=CKV_AWS_145: KMS encryption with CMK not currently supported + #checkov:skip=CKV_AWS_18: S3 access logs not necessary + #checkov:skip=CKV2_AWS_62: Event notifications not necessary + #checkov:skip=CKV2_AWS_61: Lifecycle configuration not necessary for config bucket + #checkov:skip=CKV2_AWS_6: S3 Buckets have public access blocked by default + + tags = merge(var.tags, { + Name = "${local.name_prefix}-config-${random_string.broker_config.result}" + }) +} + +resource "aws_s3_bucket_public_access_block" "broker_config" { + bucket = aws_s3_bucket.broker_config.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_versioning" "broker_config" { + bucket = aws_s3_bucket.broker_config.id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "broker_config" { + bucket = aws_s3_bucket.broker_config.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + } + } +} + +########################################## +# S3 | Broker Config Object +########################################## +resource "aws_s3_object" "broker_config" { + bucket = aws_s3_bucket.broker_config.id + key = "p4broker.conf" + content = templatefile("${path.module}/templates/p4broker.conf.tftpl", { + p4_target = var.p4_target + listen_port = var.container_port + command_rules = var.broker_command_rules + }) + + tags = merge(var.tags, { + Name = "${local.name_prefix}-broker-config" + }) +} diff --git a/modules/perforce/modules/p4-broker/data.tf b/modules/perforce/modules/p4-broker/data.tf new file mode 100644 index 00000000..b8478a82 --- /dev/null +++ b/modules/perforce/modules/p4-broker/data.tf @@ -0,0 +1,6 @@ +data "aws_region" "current" {} + +data "aws_ecs_cluster" "cluster" { + count = var.cluster_name != null ? 1 : 0 + cluster_name = var.cluster_name +} diff --git a/modules/perforce/modules/p4-broker/iam.tf b/modules/perforce/modules/p4-broker/iam.tf new file mode 100644 index 00000000..a552eea2 --- /dev/null +++ b/modules/perforce/modules/p4-broker/iam.tf @@ -0,0 +1,133 @@ +########################################## +# Random Strings +########################################## +resource "random_string" "p4_broker" { + length = 2 + special = false + upper = false +} + +########################################## +# Trust Relationships +########################################## +data "aws_iam_policy_document" "ecs_tasks_trust_relationship" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +########################################## +# Policies +########################################## +# Default Policy Document (Task Role) +data "aws_iam_policy_document" "default_policy" { + count = var.create_default_role ? 1 : 0 + + # ECS Exec support + statement { + sid = "ECSExec" + effect = "Allow" + actions = [ + "ssmmessages:OpenDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:CreateControlChannel" + ] + resources = ["*"] + } + + # S3 read access for broker config + statement { + sid = "S3ConfigRead" + effect = "Allow" + actions = [ + "s3:GetObject" + ] + resources = [ + "${aws_s3_bucket.broker_config.arn}/*" + ] + } +} + +# S3 Config Policy Document (Task Execution Role - for init container) +data "aws_iam_policy_document" "s3_config_policy" { + statement { + effect = "Allow" + actions = [ + "s3:GetObject" + ] + resources = [ + "${aws_s3_bucket.broker_config.arn}/*" + ] + } +} + +# Default Policy +resource "aws_iam_policy" "default_policy" { + count = var.create_default_role ? 1 : 0 + + name = "${local.name_prefix}-default-policy" + description = "Policy granting permissions for ${local.name_prefix}." + policy = data.aws_iam_policy_document.default_policy[0].json + + tags = merge(var.tags, { + Name = "${local.name_prefix}-default-policy" + }) +} + +# S3 Config Policy +resource "aws_iam_policy" "s3_config_policy" { + name = "${local.name_prefix}-s3-config-policy" + description = "Policy granting permissions for ${local.name_prefix} task execution role to read config from S3." + policy = data.aws_iam_policy_document.s3_config_policy.json + + tags = merge(var.tags, { + Name = "${local.name_prefix}-s3-config-policy" + }) +} + + +########################################## +# Roles +########################################## +# Default Role (Task Role) +resource "aws_iam_role" "default_role" { + count = var.create_default_role ? 1 : 0 + name = "${local.name_prefix}-default-role" + assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json + + tags = merge(var.tags, { + Name = "${local.name_prefix}-default-role" + }) +} + +resource "aws_iam_role_policy_attachment" "default_role" { + count = var.create_default_role ? 1 : 0 + role = aws_iam_role.default_role[0].name + policy_arn = aws_iam_policy.default_policy[0].arn +} + +# Task Execution Role +resource "aws_iam_role" "task_execution_role" { + name = "${local.name_prefix}-task-execution-role" + assume_role_policy = data.aws_iam_policy_document.ecs_tasks_trust_relationship.json + + tags = merge(var.tags, { + Name = "${local.name_prefix}-task-execution-role" + }) +} + +resource "aws_iam_role_policy_attachment" "task_execution_role_ecs" { + role = aws_iam_role.task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role_policy_attachment" "task_execution_role_s3_config" { + role = aws_iam_role.task_execution_role.name + policy_arn = aws_iam_policy.s3_config_policy.arn +} diff --git a/modules/perforce/modules/p4-broker/locals.tf b/modules/perforce/modules/p4-broker/locals.tf new file mode 100644 index 00000000..4e3c2ef9 --- /dev/null +++ b/modules/perforce/modules/p4-broker/locals.tf @@ -0,0 +1,5 @@ +locals { + name_prefix = "${var.project_prefix}-${var.name}" + config_volume_name = "p4-broker-config" + config_path = "/config" +} diff --git a/modules/perforce/modules/p4-broker/main.tf b/modules/perforce/modules/p4-broker/main.tf new file mode 100644 index 00000000..be8ec9a1 --- /dev/null +++ b/modules/perforce/modules/p4-broker/main.tf @@ -0,0 +1,190 @@ +########################################## +# ECS | Cluster +########################################## +# If cluster name is not provided create a new cluster +resource "aws_ecs_cluster" "cluster" { + count = var.cluster_name != null ? 0 : 1 + name = "${local.name_prefix}-cluster" + + setting { + name = "containerInsights" + value = "enabled" + } + + tags = merge(var.tags, { + Name = "${local.name_prefix}-cluster" + }) +} + + +########################################## +# ECS Cluster | Capacity Providers +########################################## +resource "aws_ecs_cluster_capacity_providers" "cluster_fargate_providers" { + count = var.cluster_name != null ? 0 : 1 + cluster_name = aws_ecs_cluster.cluster[0].name + + capacity_providers = ["FARGATE"] + + default_capacity_provider_strategy { + base = 1 + weight = 100 + capacity_provider = "FARGATE" + } +} + + +########################################## +# ECS | Task Definition +########################################## +resource "aws_ecs_task_definition" "task_definition" { + family = "${local.name_prefix}-task-definition" + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + cpu = var.container_cpu + memory = var.container_memory + + volume { + name = local.config_volume_name + } + + container_definitions = jsonencode([ + { + name = var.container_name, + image = var.container_image, + cpu = var.container_cpu, + memory = var.container_memory, + essential = true, + portMappings = [ + { + containerPort = var.container_port, + hostPort = var.container_port, + protocol = "tcp" + } + ] + environment = concat( + [], + var.extra_env != null ? [for key, value in var.extra_env : { + name = key + value = value + }] : [], + ) + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = aws_cloudwatch_log_group.log_group.name + awslogs-region = data.aws_region.current.name + awslogs-stream-prefix = "${local.name_prefix}-service" + } + } + mountPoints = [ + { + sourceVolume = local.config_volume_name + containerPath = local.config_path + } + ] + healthCheck = { + command = [ + "CMD-SHELL", "cat /proc/net/tcp | grep $(printf '%X' ${var.container_port}) || exit 1" + ] + startPeriod = 30 + } + dependsOn = [ + { + containerName = "${var.container_name}-config" + condition = "COMPLETE" + } + ] + }, + { + name = "${var.container_name}-config" + image = "amazon/aws-cli" + essential = false + command = ["s3", "cp", "s3://${aws_s3_bucket.broker_config.id}/${aws_s3_object.broker_config.key}", "${local.config_path}/p4broker.conf"] + readonly_root_filesystem = false + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = aws_cloudwatch_log_group.log_group.name + awslogs-region = data.aws_region.current.name + awslogs-stream-prefix = "${local.name_prefix}-service-config" + } + } + mountPoints = [ + { + sourceVolume = local.config_volume_name + containerPath = local.config_path + } + ] + } + ]) + + task_role_arn = var.custom_role != null ? var.custom_role : aws_iam_role.default_role[0].arn + execution_role_arn = aws_iam_role.task_execution_role.arn + + runtime_platform { + operating_system_family = "LINUX" + cpu_architecture = "X86_64" + } + + tags = merge(var.tags, { + Name = "${local.name_prefix}-task-definition" + }) +} + + +########################################## +# ECS | Service +########################################## +resource "aws_ecs_service" "service" { + name = "${local.name_prefix}-service" + + cluster = var.cluster_name != null ? data.aws_ecs_cluster.cluster[0].arn : aws_ecs_cluster.cluster[0].arn + task_definition = aws_ecs_task_definition.task_definition.arn + launch_type = "FARGATE" + desired_count = var.desired_count + + force_delete = true + force_new_deployment = var.debug + enable_execute_command = var.debug + + wait_for_steady_state = true + + load_balancer { + target_group_arn = aws_lb_target_group.nlb_target_group.arn + container_name = var.container_name + container_port = var.container_port + } + + network_configuration { + subnets = var.subnets + security_groups = [aws_security_group.ecs_service.id] + } + + tags = merge(var.tags, { + Name = "${local.name_prefix}-service" + }) + + lifecycle { + ignore_changes = [desired_count] + } + + timeouts { + create = "20m" + } + + depends_on = [aws_lb_target_group.nlb_target_group] +} + + +########################################## +# CloudWatch +########################################## +resource "aws_cloudwatch_log_group" "log_group" { + #checkov:skip=CKV_AWS_158: KMS Encryption disabled by default + name = "${local.name_prefix}-log-group" + retention_in_days = var.cloudwatch_log_retention_in_days + tags = merge(var.tags, { + Name = "${local.name_prefix}-log-group" + }) +} diff --git a/modules/perforce/modules/p4-broker/nlb.tf b/modules/perforce/modules/p4-broker/nlb.tf new file mode 100644 index 00000000..a63f4ce4 --- /dev/null +++ b/modules/perforce/modules/p4-broker/nlb.tf @@ -0,0 +1,22 @@ +########################################## +# NLB | Target Group +########################################## +resource "aws_lb_target_group" "nlb_target_group" { + name = "${local.name_prefix}-tg" + port = var.container_port + protocol = "TCP" + target_type = "ip" + vpc_id = var.vpc_id + + health_check { + protocol = "TCP" + port = "traffic-port" + healthy_threshold = 3 + unhealthy_threshold = 3 + interval = 30 + } + + tags = merge(var.tags, { + Name = "${local.name_prefix}-tg" + }) +} diff --git a/modules/perforce/modules/p4-broker/outputs.tf b/modules/perforce/modules/p4-broker/outputs.tf new file mode 100644 index 00000000..99405d4f --- /dev/null +++ b/modules/perforce/modules/p4-broker/outputs.tf @@ -0,0 +1,29 @@ +output "service_security_group_id" { + value = aws_security_group.ecs_service.id + description = "Security group associated with the ECS service running P4 Broker" +} + +output "cluster_name" { + value = var.cluster_name != null ? var.cluster_name : aws_ecs_cluster.cluster[0].name + description = "Name of the ECS cluster hosting P4 Broker" +} + +output "target_group_arn" { + value = aws_lb_target_group.nlb_target_group.arn + description = "The NLB target group ARN for P4 Broker" +} + +output "service_arn" { + value = aws_ecs_service.service.id + description = "The ARN of the P4 Broker ECS service" +} + +output "task_definition_arn" { + value = aws_ecs_task_definition.task_definition.arn + description = "The ARN of the P4 Broker task definition" +} + +output "config_bucket_name" { + value = aws_s3_bucket.broker_config.id + description = "The name of the S3 bucket containing the broker configuration" +} diff --git a/modules/perforce/modules/p4-broker/sg.tf b/modules/perforce/modules/p4-broker/sg.tf new file mode 100644 index 00000000..501c1530 --- /dev/null +++ b/modules/perforce/modules/p4-broker/sg.tf @@ -0,0 +1,27 @@ +######################################## +# ECS Service Security Group +######################################## +resource "aws_security_group" "ecs_service" { + name = "${local.name_prefix}-service" + vpc_id = var.vpc_id + description = "${local.name_prefix} service Security Group" + tags = merge(var.tags, { + Name = "${local.name_prefix}-service" + }) +} + +# Outbound access from Containers to Internet (IPV4) +resource "aws_vpc_security_group_egress_rule" "ecs_service_outbound_to_internet_ipv4" { + security_group_id = aws_security_group.ecs_service.id + description = "Allow outbound traffic from ${local.name_prefix} service to internet (ipv4)" + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" # semantically equivalent to all ports +} + +# Outbound access from Containers to Internet (IPV6) +resource "aws_vpc_security_group_egress_rule" "ecs_service_outbound_to_internet_ipv6" { + security_group_id = aws_security_group.ecs_service.id + description = "Allow outbound traffic from ${local.name_prefix} service to internet (ipv6)" + cidr_ipv6 = "::/0" + ip_protocol = "-1" # semantically equivalent to all ports +} diff --git a/modules/perforce/modules/p4-broker/templates/p4broker.conf.tftpl b/modules/perforce/modules/p4-broker/templates/p4broker.conf.tftpl new file mode 100644 index 00000000..2849b611 --- /dev/null +++ b/modules/perforce/modules/p4-broker/templates/p4broker.conf.tftpl @@ -0,0 +1,15 @@ +target = ${p4_target}; +listen = ${listen_port}; +directory = /tmp; +logfile = /tmp/p4broker.log; + +%{ for rule in command_rules ~} +command: ${rule.command} +{ + action = ${rule.action}; +%{ if rule.message != null ~} + message = "${rule.message}"; +%{ endif ~} +} + +%{ endfor ~} diff --git a/modules/perforce/modules/p4-broker/variables.tf b/modules/perforce/modules/p4-broker/variables.tf new file mode 100644 index 00000000..ae677187 --- /dev/null +++ b/modules/perforce/modules/p4-broker/variables.tf @@ -0,0 +1,151 @@ +######################################## +# General +######################################## +variable "name" { + type = string + description = "The name attached to P4 Broker module resources." + default = "p4-broker" + + validation { + condition = length(var.name) > 1 && length(var.name) <= 50 + error_message = "The defined 'name' has too many characters (${length(var.name)}). This can cause deployment failures for AWS resources with smaller character limits. Please reduce the character count and try again." + } +} + +variable "project_prefix" { + type = string + description = "The project prefix for this workload. This is appended to the beginning of most resource names." + default = "cgd" +} + +variable "debug" { + type = bool + description = "Set this flag to enable execute command on service containers and force redeploys." + default = false +} + + +######################################## +# Compute +######################################## +variable "cluster_name" { + type = string + description = "The name of the ECS cluster to deploy the P4 Broker into. Cluster is not created if this variable is provided." + default = null +} + +variable "container_name" { + type = string + description = "The name of the P4 Broker container." + default = "p4-broker-container" + nullable = false +} + +variable "container_port" { + type = number + description = "The container port that P4 Broker listens on." + default = 1666 + nullable = false +} + +variable "container_cpu" { + type = number + description = "The CPU allotment for the P4 Broker container." + default = 1024 + nullable = false +} + +variable "container_memory" { + type = number + description = "The memory allotment for the P4 Broker container." + default = 2048 + nullable = false +} + +variable "container_image" { + type = string + description = "The Docker image URI for the P4 Broker container." +} + +variable "desired_count" { + type = number + description = "The desired number of P4 Broker ECS tasks." + default = 1 +} + + +######################################## +# Broker Configuration +######################################## +variable "p4_target" { + type = string + description = "The upstream Perforce server target (e.g., ssl:p4server:1666)." +} + +variable "broker_command_rules" { + type = list(object({ + command = string + action = string + message = optional(string, null) + })) + description = "Command filtering rules for the P4 Broker configuration." + default = [{ + command = "*" + action = "pass" + message = null + }] +} + +variable "extra_env" { + type = map(string) + description = "Extra environment variables to set on the P4 Broker container." + default = null +} + + +######################################## +# Storage & Logging +######################################## +variable "cloudwatch_log_retention_in_days" { + type = number + description = "The log retention in days of the CloudWatch log group for P4 Broker." + default = 365 +} + + +######################################## +# Networking & Security +######################################## +variable "vpc_id" { + type = string + description = "The ID of the existing VPC you would like to deploy P4 Broker into." +} + +variable "subnets" { + type = list(string) + description = "A list of subnets to deploy the P4 Broker ECS Service into. Private subnets are recommended." +} + +variable "create_default_role" { + type = bool + description = "Optional creation of P4 Broker default IAM Role. Default is set to true." + default = true +} + +variable "custom_role" { + type = string + description = "ARN of the custom IAM Role you wish to use with P4 Broker." + default = null +} + +variable "tags" { + type = map(any) + description = "Tags to apply to resources." + default = { + "IaC" = "Terraform" + "ModuleBy" = "CGD-Toolkit" + "RootModuleName" = "terraform-aws-perforce" + "ModuleName" = "p4-broker" + "ModuleSource" = "https://github.com/aws-games/cloud-game-development-toolkit/tree/main/modules/perforce" + } +} diff --git a/modules/perforce/tests/integration/setup/versions.tf b/modules/perforce/modules/p4-broker/versions.tf similarity index 64% rename from modules/perforce/tests/integration/setup/versions.tf rename to modules/perforce/modules/p4-broker/versions.tf index 0d42e7a6..36859bed 100644 --- a/modules/perforce/tests/integration/setup/versions.tf +++ b/modules/perforce/modules/p4-broker/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = "~> 6.6" } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } } } diff --git a/modules/perforce/outputs.tf b/modules/perforce/outputs.tf index 40df0279..e855b0c3 100644 --- a/modules/perforce/outputs.tf +++ b/modules/perforce/outputs.tf @@ -117,8 +117,30 @@ output "p4_code_review_execution_role_id" { description = "The default role for the P4 Code Review service task" } +# P4 Broker +output "p4_broker_service_security_group_id" { + value = var.p4_broker_config != null ? module.p4_broker[0].service_security_group_id : null + description = "Security group associated with the ECS service running P4 Broker." +} + +output "p4_broker_cluster_name" { + value = var.p4_broker_config != null ? module.p4_broker[0].cluster_name : null + description = "Name of the ECS cluster hosting P4 Broker." +} + +output "p4_broker_target_group_arn" { + value = var.p4_broker_config != null ? module.p4_broker[0].target_group_arn : null + description = "The NLB target group ARN for P4 Broker." +} + +output "p4_broker_config_bucket_name" { + value = var.p4_broker_config != null ? module.p4_broker[0].config_bucket_name : null + description = "The name of the S3 bucket containing the P4 Broker configuration." +} + + output "p4_server_lambda_link_name" { - value = (var.p4_server_config.storage_type == "FSxN" && var.p4_server_config.protocol == "ISCSI" ? + value = (var.p4_server_config != null && var.p4_server_config.storage_type == "FSxN" && var.p4_server_config.protocol == "ISCSI" ? module.p4_server[0].lambda_link_name : null) description = "The name of the Lambda link for the P4 Server instance to use with FSxN." } diff --git a/modules/perforce/sg.tf b/modules/perforce/sg.tf index edf76b9f..c2770072 100644 --- a/modules/perforce/sg.tf +++ b/modules/perforce/sg.tf @@ -26,7 +26,7 @@ resource "aws_security_group" "perforce_network_load_balancer" { # Perforce NLB --> Perforce Web Services ALB # Allows Perforce NLB to send outbound traffic to Perforce Web Services ALB resource "aws_vpc_security_group_egress_rule" "perforce_nlb_outbound_to_perforce_web_services_alb" { - count = var.create_default_sgs && var.create_shared_network_load_balancer ? 1 : 0 + count = var.create_default_sgs && var.create_shared_network_load_balancer && var.create_shared_application_load_balancer ? 1 : 0 security_group_id = aws_security_group.perforce_network_load_balancer[0].id description = "Allows Perforce NLB to send outbound traffic to Perforce Web Services ALB." from_port = 443 @@ -190,3 +190,63 @@ resource "aws_vpc_security_group_egress_rule" "p4_code_review_outbound_to_p4_ser ip_protocol = "TCP" referenced_security_group_id = module.p4_server[0].security_group_id } + + +############################################################################################ +# P4 Broker Security Group | Rules (security group itself is created in the submodule) +############################################################################################ +# Perforce NLB --> P4 Broker +# Allows Perforce NLB to send outbound traffic to P4 Broker +resource "aws_vpc_security_group_egress_rule" "perforce_nlb_outbound_to_p4_broker" { + count = var.p4_broker_config != null && var.create_default_sgs && var.create_shared_network_load_balancer ? 1 : 0 + security_group_id = aws_security_group.perforce_network_load_balancer[0].id + description = "Allows Perforce NLB to send outbound traffic to P4 Broker." + from_port = var.p4_broker_config.container_port + to_port = var.p4_broker_config.container_port + ip_protocol = "TCP" + referenced_security_group_id = module.p4_broker[0].service_security_group_id + + tags = merge(var.tags, { + Name = "${var.project_prefix}-perforce-nlb-to-p4-broker-sg-rule" + }) +} + +# P4 Broker <-- Perforce NLB +# Allows P4 Broker to receive inbound traffic from Perforce NLB +resource "aws_vpc_security_group_ingress_rule" "p4_broker_inbound_from_perforce_nlb" { + count = var.p4_broker_config != null && var.create_default_sgs && var.create_shared_network_load_balancer ? 1 : 0 + security_group_id = module.p4_broker[0].service_security_group_id + description = "Allows P4 Broker to receive inbound traffic from Perforce NLB." + ip_protocol = "TCP" + from_port = var.p4_broker_config.container_port + to_port = var.p4_broker_config.container_port + referenced_security_group_id = aws_security_group.perforce_network_load_balancer[0].id + + tags = merge(var.tags, { + Name = "${var.project_prefix}-p4-broker-from-nlb-sg-rule" + }) +} + +# P4 Broker --> P4 Server +# Allows P4 Broker to send outbound traffic to P4 Server (upstream) +resource "aws_vpc_security_group_egress_rule" "p4_broker_outbound_to_p4_server" { + count = var.p4_broker_config != null && var.p4_server_config != null && var.create_default_sgs ? 1 : 0 + security_group_id = module.p4_broker[0].service_security_group_id + description = "Allows P4 Broker to send outbound traffic to P4 Server." + from_port = 1666 + to_port = 1666 + ip_protocol = "TCP" + referenced_security_group_id = module.p4_server[0].security_group_id +} + +# P4 Server <-- P4 Broker +# Allows P4 Server to receive inbound traffic from P4 Broker +resource "aws_vpc_security_group_ingress_rule" "p4_server_inbound_from_p4_broker" { + count = var.p4_broker_config != null && var.p4_server_config != null && var.create_default_sgs ? 1 : 0 + security_group_id = module.p4_server[0].security_group_id + description = "Allows P4 Server to receive inbound traffic from P4 Broker." + ip_protocol = "TCP" + from_port = 1666 + to_port = 1666 + referenced_security_group_id = module.p4_broker[0].service_security_group_id +} diff --git a/modules/perforce/tests/unit/01_conditional_creation.tftest.hcl b/modules/perforce/tests/01_conditional_creation.tftest.hcl similarity index 81% rename from modules/perforce/tests/unit/01_conditional_creation.tftest.hcl rename to modules/perforce/tests/01_conditional_creation.tftest.hcl index 2150b89f..fd1c3464 100644 --- a/modules/perforce/tests/unit/01_conditional_creation.tftest.hcl +++ b/modules/perforce/tests/01_conditional_creation.tftest.hcl @@ -40,14 +40,7 @@ mock_provider "aws" { mock_data "aws_iam_policy_document" { defaults = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Action = "*" - Resource = "*" - }] - }) + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}]}" } } @@ -71,7 +64,10 @@ run "no_submodules" { command = plan variables { - vpc_id = "vpc-12345678" + vpc_id = "vpc-12345678" + create_shared_network_load_balancer = false + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false # All submodule configs are null (default) } @@ -101,9 +97,11 @@ run "p4_server_only" { command = plan variables { - vpc_id = "vpc-12345678" - shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false p4_server_config = { fully_qualified_domain_name = "p4.test.internal" @@ -141,9 +139,11 @@ run "p4_auth_only" { command = plan variables { - vpc_id = "vpc-12345678" - shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + vpc_id = "vpc-12345678" + shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + create_shared_network_load_balancer = false + create_route53_private_hosted_zone = false p4_auth_config = { fully_qualified_domain_name = "auth.test.internal" @@ -178,9 +178,11 @@ run "p4_code_review_only" { command = plan variables { - vpc_id = "vpc-12345678" - shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + vpc_id = "vpc-12345678" + shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + create_shared_network_load_balancer = false + create_route53_private_hosted_zone = false p4_code_review_config = { fully_qualified_domain_name = "swarm.test.internal" @@ -216,10 +218,11 @@ run "server_and_auth" { command = plan variables { - vpc_id = "vpc-12345678" - shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + create_route53_private_hosted_zone = false p4_server_config = { fully_qualified_domain_name = "p4.test.internal" @@ -262,10 +265,11 @@ run "server_and_code_review" { command = plan variables { - vpc_id = "vpc-12345678" - shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + create_route53_private_hosted_zone = false p4_server_config = { fully_qualified_domain_name = "p4.test.internal" @@ -317,7 +321,7 @@ run "full_stack" { route53_private_hosted_zone_name = "perforce.internal" p4_server_config = { - fully_qualified_domain_name = "p4.perforce.internal" + fully_qualified_domain_name = "perforce.internal" instance_subnet_id = "subnet-111" p4_server_type = "p4d_commit" depot_volume_size = 128 @@ -368,11 +372,12 @@ run "full_stack_existing_ecs_cluster" { command = plan variables { - vpc_id = "vpc-12345678" - shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" - existing_ecs_cluster_name = "my-existing-cluster" + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + existing_ecs_cluster_name = "my-existing-cluster" + create_route53_private_hosted_zone = false p4_server_config = { fully_qualified_domain_name = "p4.test.internal" diff --git a/modules/perforce/tests/unit/02_shared_resources.tftest.hcl b/modules/perforce/tests/02_shared_resources.tftest.hcl similarity index 73% rename from modules/perforce/tests/unit/02_shared_resources.tftest.hcl rename to modules/perforce/tests/02_shared_resources.tftest.hcl index 3f8ebaeb..5973573c 100644 --- a/modules/perforce/tests/unit/02_shared_resources.tftest.hcl +++ b/modules/perforce/tests/02_shared_resources.tftest.hcl @@ -29,10 +29,7 @@ mock_provider "aws" { } mock_data "aws_iam_policy_document" { defaults = { - json = jsonencode({ - Version = "2012-10-17" - Statement = [{ Effect = "Allow", Action = "*", Resource = "*" }] - }) + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}]}" } } mock_data "aws_ami" { @@ -51,9 +48,11 @@ run "ecs_cluster_auth_only" { command = plan variables { - vpc_id = "vpc-12345678" - shared_alb_subnets = ["subnet-111", "subnet-222"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + vpc_id = "vpc-12345678" + shared_alb_subnets = ["subnet-111", "subnet-222"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + create_shared_network_load_balancer = false + create_route53_private_hosted_zone = false p4_auth_config = { fully_qualified_domain_name = "auth.test.internal" @@ -77,9 +76,11 @@ run "ecs_cluster_code_review_only" { command = plan variables { - vpc_id = "vpc-12345678" - shared_alb_subnets = ["subnet-111", "subnet-222"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + vpc_id = "vpc-12345678" + shared_alb_subnets = ["subnet-111", "subnet-222"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + create_shared_network_load_balancer = false + create_route53_private_hosted_zone = false p4_code_review_config = { fully_qualified_domain_name = "swarm.test.internal" @@ -103,10 +104,12 @@ run "ecs_cluster_shared" { command = plan variables { - vpc_id = "vpc-12345678" - shared_alb_subnets = ["subnet-111", "subnet-222"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" - shared_ecs_cluster_name = "my-shared-cluster" + vpc_id = "vpc-12345678" + shared_alb_subnets = ["subnet-111", "subnet-222"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + shared_ecs_cluster_name = "my-shared-cluster" + create_shared_network_load_balancer = false + create_route53_private_hosted_zone = false p4_auth_config = { fully_qualified_domain_name = "auth.test.internal" @@ -143,7 +146,7 @@ run "route53_private_zone" { route53_private_hosted_zone_name = "perforce.internal" p4_server_config = { - fully_qualified_domain_name = "p4.perforce.internal" + fully_qualified_domain_name = "perforce.internal" instance_subnet_id = "subnet-111" p4_server_type = "p4d_commit" } @@ -170,11 +173,12 @@ run "load_balancer_access_logs" { command = plan variables { - vpc_id = "vpc-12345678" - shared_nlb_subnets = ["subnet-111", "subnet-222"] - shared_alb_subnets = ["subnet-111", "subnet-222"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" - enable_shared_lb_access_logs = true + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222"] + shared_alb_subnets = ["subnet-111", "subnet-222"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + enable_shared_lb_access_logs = true + create_route53_private_hosted_zone = false p4_server_config = { fully_qualified_domain_name = "p4.test.internal" @@ -189,12 +193,12 @@ run "load_balancer_access_logs" { } assert { - condition = length(aws_s3_bucket.lb_access_logs) == 1 + condition = length(aws_s3_bucket.shared_lb_access_logs_bucket) == 1 error_message = "S3 bucket should be created when load balancer access logging is enabled" } assert { - condition = length(aws_lb.perforce_shared_nlb) > 0 ? aws_lb.perforce_shared_nlb[0].enable_cross_zone_load_balancing == true : true + condition = aws_lb.perforce[0].enable_cross_zone_load_balancing == true error_message = "NLB should have cross-zone load balancing enabled" } } @@ -204,9 +208,11 @@ run "no_ecs_cluster_server_only" { command = plan variables { - vpc_id = "vpc-12345678" - shared_nlb_subnets = ["subnet-111", "subnet-222"] - certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test" + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false p4_server_config = { fully_qualified_domain_name = "p4.test.internal" diff --git a/modules/perforce/tests/03_p4_broker.tftest.hcl b/modules/perforce/tests/03_p4_broker.tftest.hcl new file mode 100644 index 00000000..0b1ba947 --- /dev/null +++ b/modules/perforce/tests/03_p4_broker.tftest.hcl @@ -0,0 +1,244 @@ +# Test: P4 Broker Conditional Creation and Resource Validation +# This test validates that the P4 Broker submodule is correctly created or skipped +# based on provided configuration, and that related shared resources behave correctly. + +# Mock providers (required in each test file) +mock_provider "aws" { + mock_data "aws_region" { + defaults = { name = "us-east-1", id = "us-east-1" } + } + mock_data "aws_caller_identity" { + defaults = { + account_id = "123456789012" + arn = "arn:aws:iam::123456789012:user/test" + user_id = "AIDACKCEVSQ6C2EXAMPLE" + } + } + mock_data "aws_elb_service_account" { + defaults = { + arn = "arn:aws:iam::127311923021:root" + id = "127311923021" + } + } + mock_data "aws_ecs_cluster" { + defaults = { + arn = "arn:aws:ecs:us-east-1:123456789012:cluster/existing-cluster" + id = "existing-cluster" + name = "existing-cluster" + status = "ACTIVE" + pending_tasks_count = 0 + running_tasks_count = 0 + } + } + mock_data "aws_iam_policy_document" { + defaults = { + json = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}]}" + } + } + mock_data "aws_ami" { + defaults = { id = "ami-0123456789abcdef0", architecture = "x86_64" } + } +} + +mock_provider "awscc" {} +mock_provider "random" {} +mock_provider "null" {} +mock_provider "local" {} +mock_provider "netapp-ontap" {} + +# Test 1: P4 Broker NOT created when config is null +run "broker_not_created_when_null" { + command = plan + + variables { + vpc_id = "vpc-12345678" + create_shared_network_load_balancer = false + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false + # p4_broker_config is null by default + } + + assert { + condition = length(module.p4_broker) == 0 + error_message = "P4 Broker submodule should not be created when p4_broker_config is null" + } +} + +# Test 2: P4 Broker created when config is provided +run "broker_created_when_configured" { + command = plan + + variables { + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222"] + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false + + p4_broker_config = { + container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/p4-broker:latest" + p4_target = "ssl:p4server:1666" + service_subnets = ["subnet-111", "subnet-222"] + } + } + + assert { + condition = length(module.p4_broker) == 1 + error_message = "P4 Broker submodule should be created when p4_broker_config is provided" + } + + assert { + condition = length(aws_ecs_cluster.perforce_web_services_cluster) == 1 + error_message = "ECS cluster should be created when P4 Broker is deployed" + } +} + +# Test 3: Shared ECS cluster created when only broker is configured +run "ecs_cluster_broker_only" { + command = plan + + variables { + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222"] + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false + + p4_broker_config = { + container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/p4-broker:latest" + p4_target = "ssl:p4server:1666" + service_subnets = ["subnet-111", "subnet-222"] + } + } + + assert { + condition = local.create_shared_ecs_cluster == true + error_message = "create_shared_ecs_cluster should be true when P4 Broker is deployed and no existing cluster provided" + } + + assert { + condition = length(aws_ecs_cluster.perforce_web_services_cluster) == 1 + error_message = "Shared ECS cluster should be created when only P4 Broker is deployed" + } +} + +# Test 4: NLB listener created for broker traffic +run "nlb_listener_created_for_broker" { + command = plan + + variables { + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222"] + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false + + p4_broker_config = { + container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/p4-broker:latest" + p4_target = "ssl:p4server:1666" + service_subnets = ["subnet-111", "subnet-222"] + } + } + + assert { + condition = length(aws_lb_listener.perforce_broker) == 1 + error_message = "NLB TCP listener should be created for P4 Broker traffic" + } +} + +# Test 5: P4 Broker with existing ECS cluster +run "broker_with_existing_cluster" { + command = plan + + variables { + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222"] + existing_ecs_cluster_name = "my-existing-cluster" + create_shared_application_load_balancer = false + create_route53_private_hosted_zone = false + + p4_broker_config = { + container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/p4-broker:latest" + p4_target = "ssl:p4server:1666" + service_subnets = ["subnet-111", "subnet-222"] + } + } + + assert { + condition = length(module.p4_broker) == 1 + error_message = "P4 Broker submodule should be created when p4_broker_config is provided" + } + + assert { + condition = length(aws_ecs_cluster.perforce_web_services_cluster) == 0 + error_message = "ECS cluster should not be created when existing_ecs_cluster_name is provided" + } + + assert { + condition = local.create_shared_ecs_cluster == false + error_message = "create_shared_ecs_cluster should be false when existing cluster is provided" + } +} + +# Test 6: Full stack with broker +run "full_stack_with_broker" { + command = plan + + variables { + vpc_id = "vpc-12345678" + shared_nlb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + shared_alb_subnets = ["subnet-111", "subnet-222", "subnet-333"] + certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/test-cert" + + create_route53_private_hosted_zone = true + route53_private_hosted_zone_name = "perforce.internal" + + p4_server_config = { + fully_qualified_domain_name = "perforce.internal" + instance_subnet_id = "subnet-111" + p4_server_type = "p4d_commit" + depot_volume_size = 128 + metadata_volume_size = 32 + logs_volume_size = 32 + } + + p4_auth_config = { + fully_qualified_domain_name = "auth.perforce.internal" + service_subnets = ["subnet-111", "subnet-222", "subnet-333"] + } + + p4_code_review_config = { + fully_qualified_domain_name = "swarm.perforce.internal" + service_subnets = ["subnet-111", "subnet-222", "subnet-333"] + enable_sso = true + } + + p4_broker_config = { + container_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/p4-broker:latest" + p4_target = "ssl:p4.perforce.internal:1666" + service_subnets = ["subnet-111", "subnet-222", "subnet-333"] + } + } + + assert { + condition = length(module.p4_server) == 1 + error_message = "P4 Server submodule should be created when p4_server_config is provided" + } + + assert { + condition = length(module.p4_auth) == 1 + error_message = "P4 Auth submodule should be created when p4_auth_config is provided" + } + + assert { + condition = length(module.p4_code_review) == 1 + error_message = "P4 Code Review submodule should be created when p4_code_review_config is provided" + } + + assert { + condition = length(module.p4_broker) == 1 + error_message = "P4 Broker submodule should be created when p4_broker_config is provided" + } + + assert { + condition = length(aws_ecs_cluster.perforce_web_services_cluster) == 1 + error_message = "ECS cluster should be created when web services are deployed" + } +} diff --git a/modules/perforce/tests/README.md b/modules/perforce/tests/README.md index becd9dec..22f46c2d 100644 --- a/modules/perforce/tests/README.md +++ b/modules/perforce/tests/README.md @@ -1,292 +1,36 @@ # Perforce Module Tests -This directory contains comprehensive tests for the Perforce wrapper module, organized into unit tests and integration tests. +Mock-based unit tests for the Perforce wrapper module. All tests use mock providers and require no AWS credentials. ## Test Structure ```text tests/ -├── unit/ # Mock-based unit tests -│ ├── 01_conditional_creation.tftest.hcl # Tests submodule conditional creation -│ ├── 02_shared_resources.tftest.hcl # Tests shared resource logic -│ └── README.md # Unit test documentation -│ -├── integration/ # Integration tests with real AWS -│ ├── setup/ # Setup module for SSM parameters -│ │ ├── ssm.tf # SSM parameter data sources -│ │ └── versions.tf # Provider requirements -│ ├── 01_create_resources_complete.tftest.hcl -│ ├── 02_p4_server_fsxn.tftest.hcl -│ └── README.md # Integration test documentation -│ -└── README.md # This file +├── 01_conditional_creation.tftest.hcl # Submodule conditional creation +├── 02_shared_resources.tftest.hcl # Shared resource logic (ECS cluster, Route53, LBs) +├── 03_p4_broker.tftest.hcl # P4 Broker creation and integration +└── README.md ``` -## Quick Start - -### Run All Tests - -```bash -cd /path/to/modules/perforce -terraform test -``` - -### Run Only Unit Tests (No AWS Required) - -```bash -terraform test -filter=tests/unit/ -``` - -### Run Only Integration Tests (AWS Required) - -```bash -export AWS_PROFILE=your-profile -terraform test -filter=tests/integration/ -``` - -## Test Types - -### Unit Tests (`unit/`) - -**Purpose:** Validate conditional logic and resource creation without deploying infrastructure - -**Characteristics:** - -- ✅ Uses mock providers (no AWS credentials needed) -- ✅ Fast execution (seconds) -- ✅ Safe to run anywhere -- ✅ Tests all conditional logic paths -- ✅ No AWS costs - -**When to Run:** On every code change, in CI/CD pipelines, during development - -**Test Coverage:** - -- Conditional creation of P4 Server, P4 Auth, and P4 Code Review submodules -- Shared ECS cluster creation logic -- Load balancer and Route53 resource creation -- Security group configurations - -[📖 Unit Tests Documentation](unit/README.md) - -### Integration Tests (`integration/`) - -**Purpose:** Validate that example deployments work with real AWS resources - -**Characteristics:** - -- ⚠️ Requires AWS credentials -- ⚠️ Slower execution (minutes) -- ⚠️ Plans against real AWS (no apply by default) -- ✅ Tests real-world scenarios -- ✅ Validates examples work correctly - -**When to Run:** Before releases, when testing infrastructure changes, in CI/CD with AWS access - -**Test Coverage:** - -- Complete Perforce deployment example -- P4 Server with FSxN storage example -- Example configurations with real parameters - -[📖 Integration Tests Documentation](integration/README.md) - -## CI/CD Integration - -### Validation Workflow - -The `terraform-validation.yml` workflow validates Terraform configurations: - -**What it validates:** - -- All directories containing `.tf` files (modules, submodules, examples, test setup) -- Runs `terraform init` and `terraform validate` -- Skips directories with only `.tftest.hcl` files - -**What triggers it:** - -- Changes to `modules/**/*.tf` or `samples/**/*.tf` -- Push to `main` branch -- Manual workflow dispatch - -### Test Workflow - -The `terraform-tests.yml` workflow runs Terraform tests: - -**What it runs:** - -- All `.tftest.hcl` files in modules with a `tests/` directory -- Automatically runs when module files change -- Requires AWS credentials for integration tests - -**Workflow behavior:** - -- Detects changed modules -- Runs `terraform test` from module root -- Reports failures to pull requests - -## Development Workflow - -### Adding New Features - -1. **Write unit tests first** - Add test scenarios to `unit/` for new conditional logic -2. **Implement the feature** - Modify module code -3. **Run unit tests** - Verify conditional logic works: `terraform test -filter=tests/unit/` -4. **Add integration tests** - If needed, add scenarios to `integration/` -5. **Run all tests** - Verify everything works: `terraform test` - -### Debugging Test Failures - -**Unit test failures:** +## Running Tests ```bash -# Run with verbose output -terraform test -filter=tests/unit/01_conditional_creation.tftest.hcl -verbose - -# Check specific assertion -# Look for "error_message" in the output to see which assertion failed -``` +# From the module root +cd modules/perforce -**Integration test failures:** - -```bash -# Verify AWS credentials -aws sts get-caller-identity +# Run all tests +terraform test -# Check SSM parameters exist -aws ssm get-parameter --name "/cloud-game-development-toolkit/modules/perforce/route53-public-hosted-zone-name" +# Run a specific test file +terraform test -filter=tests/03_p4_broker.tftest.hcl -# Run with verbose output -terraform test -filter=tests/integration/ -verbose +# Verbose output +terraform test -verbose ``` -## Test Maintenance - -### When to Update Tests - -**Update unit tests when:** - -- Adding new conditional logic -- Adding new submodules or shared resources -- Changing variable validation rules -- Modifying resource creation conditions - -**Update integration tests when:** - -- Adding new examples -- Changing example configurations -- Modifying required variables -- Adding new deployment patterns - -### Adding New Test Files - -**Unit tests:** - -1. Create new `.tftest.hcl` file in `unit/` -2. Copy mock provider blocks from existing test -3. Add test scenarios with clear names and assertions -4. Update `unit/README.md` with test documentation - -**Integration tests:** - -1. Create new `.tftest.hcl` file in `integration/` -2. Add required SSM parameters to `integration/setup/ssm.tf` -3. Reference appropriate example deployment -4. Update `integration/README.md` with test documentation - -## Best Practices - -### Writing Good Tests - -✅ **DO:** - -- Use descriptive test names (`p4_server_only` not `test1`) -- Write clear assertion error messages -- Test both success and failure scenarios -- Document complex test logic with comments -- Keep tests focused on one aspect - -❌ **DON'T:** - -- Hardcode sensitive values (use SSM for integration tests) -- Create tests that depend on execution order -- Test implementation details (test behavior, not code) -- Ignore test failures (fix or document expected failures) - -### Mock Provider Patterns - -When creating unit tests: - -1. Always include all mock providers (even if unused) -2. Use realistic mock data (valid ARNs, IDs, etc.) -3. Copy mock blocks from existing tests for consistency -4. Document any custom mock configurations - -## Performance Considerations - -### Test Execution Time - -| Test Type | Typical Duration | Parallelization | -|-----------|-----------------|-----------------| -| Unit (single file) | 2-5 seconds | Yes | -| Unit (all) | 10-15 seconds | Yes | -| Integration (single) | 30-60 seconds | Yes | -| Integration (all) | 2-5 minutes | Yes | - -### Optimizing Test Speed - -- Run unit tests during development (fast feedback) -- Run integration tests before commits (thorough validation) -- Use `-filter` to run specific tests during debugging -- Leverage Terraform's parallel test execution - -## Troubleshooting - -### Common Issues - -#### "No tests found" - -- Ensure you're running from the module root directory -- Verify `.tftest.hcl` files exist in `tests/` subdirectories - -#### "Module not found" - -- Check that module paths are relative to the test file location -- Integration tests should use `../../examples/` for example paths -- Unit tests should reference the module root - -#### "Provider configuration not found" - -- Verify all required mock providers are declared -- Check that provider versions match `versions.tf` - -#### "Variable not set" - -- Ensure all required variables are provided in test scenarios -- Check that variable types match module expectations - -## Additional Resources - -- [Terraform Testing Documentation](https://developer.hashicorp.com/terraform/language/tests) -- [Module README](../README.md) -- [Example Deployments](../examples/) -- [Horde Module Tests](../../unreal/horde/tests/) - Reference implementation - -## Contributing - -When contributing tests: - -1. Follow existing test patterns and naming conventions -2. Update documentation when adding new tests -3. Ensure tests pass locally before submitting PR -4. Add test coverage for new features -5. Keep tests maintainable and well-documented - -## Questions? - -For questions about testing: +## Adding New Tests -- Review the [unit test README](unit/README.md) for mock-based testing -- Review the [integration test README](integration/README.md) for AWS-based testing -- Check the [main module documentation](../README.md) for module usage -- Open an issue in the repository for specific problems +1. Create a new `.tftest.hcl` file in this directory +2. Copy mock provider blocks from an existing test file +3. Add test scenarios with descriptive names and clear assertion messages +4. Use `command = plan` for mock-based testing diff --git a/modules/perforce/tests/integration/01_create_resources_complete.tftest.hcl b/modules/perforce/tests/integration/01_create_resources_complete.tftest.hcl deleted file mode 100644 index ea5ae0df..00000000 --- a/modules/perforce/tests/integration/01_create_resources_complete.tftest.hcl +++ /dev/null @@ -1,27 +0,0 @@ -# Fetch relevant values from SSM Parameter Store -run "setup" { - command = plan - module { - source = "./setup" - } -} - -run "unit_test" { - command = plan - - variables { - route53_public_hosted_zone_name = run.setup.route53_public_hosted_zone_name - } - module { - source = "../../examples/create-resources-complete" - } -} - -# Unused until error handling/retry logic is improved in Terraform test -# https://github.com/hashicorp/terraform/issues/36846#issuecomment-2820247524 -# run "e2e_test" { -# command = apply -# module { -# source = "../../examples/create-resources-complete" -# } -# } diff --git a/modules/perforce/tests/integration/02_p4_server_fsxn.tftest.hcl b/modules/perforce/tests/integration/02_p4_server_fsxn.tftest.hcl deleted file mode 100644 index 0fb0c864..00000000 --- a/modules/perforce/tests/integration/02_p4_server_fsxn.tftest.hcl +++ /dev/null @@ -1,28 +0,0 @@ -# Fetch relevant values from SSM Parameter Store -run "setup" { - command = plan - module { - source = "./setup" - } -} -run "unit_test" { - command = plan - - variables { - route53_public_hosted_zone_name = run.setup.route53_public_hosted_zone_name - fsxn_password = run.setup.fsxn_password - fsxn_aws_profile = run.setup.fsxn_aws_profile - } - module { - source = "../../examples/p4-server-fsxn" - } -} - -# Unused until error handling/retry logic is improved in Terraform test -# https://github.com/hashicorp/terraform/issues/36846#issuecomment-2820247524 -# # run "e2e_test" { -# # command = apply -# # module { -# # source = "../../examples/p4-server-fsxn" -# # } -# # } diff --git a/modules/perforce/tests/integration/README.md b/modules/perforce/tests/integration/README.md deleted file mode 100644 index bc5e9580..00000000 --- a/modules/perforce/tests/integration/README.md +++ /dev/null @@ -1,171 +0,0 @@ -# Perforce Module Integration Tests - -This directory contains integration tests that validate the Perforce module against real AWS infrastructure by invoking the example deployments. - -## Overview - -Integration tests differ from unit tests in that they: - -- **Use real AWS resources** - Actual infrastructure is deployed (in plan mode for safety) -- **Test example deployments** - Validates that examples work correctly -- **Require AWS credentials** - Must have valid AWS authentication configured -- **Use SSM parameters** - Fetch configuration values from AWS Systems Manager Parameter Store - -## Test Files - -### `01_create_resources_complete.tftest.hcl` - -**Purpose:** Validates the complete Perforce deployment example - -**Example Invoked:** `examples/create-resources-complete` - -**Required SSM Parameters:** - -- `/cloud-game-development-toolkit/modules/perforce/route53-public-hosted-zone-name` - -**Test Flow:** - -1. `setup` run - Fetches Route53 zone name from SSM Parameter Store -2. `unit_test` run - Plans the complete example deployment using fetched parameters - -### `02_p4_server_fsxn.tftest.hcl` - -**Purpose:** Validates P4 Server deployment with FSxN (NetApp ONTAP) storage - -**Example Invoked:** `examples/p4-server-fsxn` - -**Required SSM Parameters:** - -- `/cloud-game-development-toolkit/modules/perforce/route53-public-hosted-zone-name` -- `/cloud-game-development-toolkit/modules/perforce/fsxn-password` -- `/cloud-game-development-toolkit/modules/perforce/fsxn-aws-profile` - -**Test Flow:** - -1. `setup` run - Fetches FSxN configuration from SSM Parameter Store -2. `unit_test` run - Plans the FSxN example deployment using fetched parameters - -## Setup Module - -The `setup/` directory contains a Terraform module that fetches test configuration from AWS Systems Manager Parameter Store. - -**Files:** - -- `setup/ssm.tf` - Data sources for SSM parameters and outputs -- `setup/versions.tf` - Terraform and provider version constraints - -**Purpose:** Centralizes test configuration management and avoids hardcoding sensitive values in test files. - -## Running Integration Tests - -### Prerequisites - -1. **AWS Credentials** - Configure AWS authentication: - - ```bash - export AWS_PROFILE=your-profile - # OR - export AWS_ACCESS_KEY_ID=xxx - export AWS_SECRET_ACCESS_KEY=xxx - ``` - -2. **SSM Parameters** - Create required parameters in your AWS account: - - ```bash - aws ssm put-parameter \ - --name "/cloud-game-development-toolkit/modules/perforce/route53-public-hosted-zone-name" \ - --value "your-domain.com" \ - --type String - ``` - -### Run All Integration Tests - -From the module root directory: - -```bash -cd /path/to/modules/perforce -terraform test -filter=tests/integration/ -``` - -### Run Specific Integration Test - -```bash -terraform test -filter=tests/integration/01_create_resources_complete.tftest.hcl -``` - -## E2E Tests (Disabled) - -The integration test files contain commented-out `e2e_test` run blocks that would deploy actual infrastructure using `command = apply`. These are currently disabled due to Terraform test error handling limitations: - -- **Issue:** [hashicorp/terraform#36846](https://github.com/hashicorp/terraform/issues/36846) -- **When to enable:** Once Terraform improves error handling and retry logic for test commands - -## Test Workflow - -Integration tests are automatically run by the `terraform-tests.yml` GitHub Actions workflow: - -**Trigger Conditions:** - -- Pull requests that modify files in `modules/**` -- Manual workflow dispatch - -**Test Discovery:** - -- Finds all modules with a `tests/` directory -- Runs `terraform test` from the module root - -**Note:** Integration tests require AWS credentials configured in the CI environment via GitHub Secrets or OIDC authentication. - -## Comparison with Unit Tests - -| Aspect | Unit Tests | Integration Tests | -|--------|-----------|-------------------| -| **Speed** | Fast (seconds) | Slow (minutes) | -| **AWS Access** | Not required | Required | -| **Infrastructure** | Mock providers | Real AWS resources | -| **Purpose** | Test conditional logic | Test example deployments | -| **When to Run** | Every commit | Before releases | -| **Cost** | Free | AWS costs (minimal with plan-only) | - -## Maintenance - -### Adding New Integration Tests - -1. Create a new `.tftest.hcl` file in this directory -2. Add required SSM parameters to `setup/ssm.tf` -3. Reference the appropriate example deployment -4. Update this README with test details - -### Updating SSM Parameters - -When test configuration changes: - -1. Update SSM parameter values in your AWS account -2. Update `setup/ssm.tf` if new parameters are needed -3. Update integration test files to use new parameters - -## Troubleshooting - -### "Parameter not found" errors - -- Verify SSM parameters exist in your AWS account -- Check parameter names match exactly (case-sensitive) -- Ensure AWS credentials have permission to read SSM parameters - -### "Access denied" errors - -- Verify your IAM role/user has required permissions -- Check that the AWS region is correct -- Ensure AWS credentials are properly configured - -### Example not found errors - -- Verify the example path is correct relative to module root -- Check that the example directory contains valid Terraform files -- Ensure you're running tests from the module root directory - -## Related Documentation - -- [Unit Tests](../unit/README.md) - Mock-based tests for conditional logic -- [Module Examples](../../examples/) - Example deployments referenced by tests -- [Main Module README](../../README.md) - Module usage and configuration diff --git a/modules/perforce/tests/integration/setup/ssm.tf b/modules/perforce/tests/integration/setup/ssm.tf deleted file mode 100644 index 69045cc9..00000000 --- a/modules/perforce/tests/integration/setup/ssm.tf +++ /dev/null @@ -1,24 +0,0 @@ -# Fetch relevant values from SSM Parameter Store -data "aws_ssm_parameter" "route53_public_hosted_zone_name" { - name = "/cloud-game-development-toolkit/modules/perforce/route53-public-hosted-zone-name" -} -data "aws_ssm_parameter" "fsxn_password" { - name = "/cloud-game-development-toolkit/modules/perforce/fsxn-password" -} -data "aws_ssm_parameter" "fsxn_aws_profile" { - name = "/cloud-game-development-toolkit/modules/perforce/fsxn-aws-profile" -} - - -output "route53_public_hosted_zone_name" { - value = nonsensitive(data.aws_ssm_parameter.route53_public_hosted_zone_name.value) - sensitive = false -} -output "fsxn_password" { - value = nonsensitive(data.aws_ssm_parameter.fsxn_password.value) - sensitive = false -} -output "fsxn_aws_profile" { - value = nonsensitive(data.aws_ssm_parameter.fsxn_aws_profile.value) - sensitive = false -} diff --git a/modules/perforce/tests/unit/README.md b/modules/perforce/tests/unit/README.md deleted file mode 100644 index da15ad9a..00000000 --- a/modules/perforce/tests/unit/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Perforce Module Unit Tests - -This directory contains mock-based unit tests for the Perforce wrapper module. These tests validate the conditional logic and resource creation without requiring AWS credentials or deploying actual infrastructure. - -## Overview - -The Perforce module is a **wrapper module** that orchestrates the deployment of three submodules: - -- **P4 Server** (`modules/p4-server/`) - Perforce Helix Core version control server -- **P4 Auth** (`modules/p4-auth/`) - Perforce authentication service -- **P4 Code Review** (`modules/p4-code-review/`) - Perforce Swarm code review platform - -Unit tests ensure that: - -- Submodules are created only when their configuration is provided -- Shared resources (ECS cluster, load balancers, Route53) are created correctly -- Various combinations of submodules work without conflicts - -## Test Files - -### `01_conditional_creation.tftest.hcl` - -**Purpose:** Validates that submodules are conditionally created based on configuration - -**Test Scenarios (8 total):** - -1. `no_submodules` - No configuration provided, no resources created -2. `p4_server_only` - Only P4 Server deployed -3. `p4_auth_only` - Only P4 Auth deployed -4. `p4_code_review_only` - Only P4 Code Review deployed (note: depends on P4 Server for credentials) -5. `server_and_auth` - P4 Server + P4 Auth combination -6. `server_and_code_review` - P4 Server + P4 Code Review combination -7. `full_stack` - All three submodules deployed together -8. `full_stack_existing_ecs_cluster` - Full stack using an existing ECS cluster - -**Key Validations:** - -- `length(module.p4_server)` equals 0 or 1 based on configuration -- `length(module.p4_auth)` equals 0 or 1 based on configuration -- `length(module.p4_code_review)` equals 0 or 1 based on configuration -- ECS cluster creation logic based on web service deployment - -### `02_shared_resources.tftest.hcl` - -**Purpose:** Validates shared resource creation logic - -**Test Scenarios (6 total):** - -1. `ecs_cluster_auth_only` - ECS cluster created for Auth service -2. `ecs_cluster_code_review_only` - ECS cluster created for Code Review service -3. `ecs_cluster_shared` - Single ECS cluster shared by both services -4. `route53_private_zone` - Private hosted zone and DNS records -5. `load_balancer_access_logs` - S3 bucket for LB access logs -6. `no_ecs_cluster_server_only` - No ECS cluster when only P4 Server is deployed - -**Key Validations:** - -- `local.create_shared_ecs_cluster` logic correctness -- Route53 zone and record configurations -- S3 bucket creation for access logs -- Load balancer configurations - -## Running Tests - -### Run All Unit Tests - -From the module root directory: - -```bash -cd /path/to/modules/perforce -terraform test -``` - -### Run Specific Test File - -```bash -terraform test -filter=tests/unit/01_conditional_creation.tftest.hcl -``` - -### Run Specific Test Scenario - -```bash -terraform test -filter=tests/unit/01_conditional_creation.tftest.hcl -verbose -``` - -## Mock Providers - -All test files use mock providers to simulate AWS resources without making actual API calls: - -- **aws** - Mocks AWS provider with data sources for region, caller identity, ELB service account, ECS cluster, IAM policy documents, and AMI -- **awscc** - Mocks AWS Cloud Control provider -- **random** - Mocks random provider -- **null** - Mocks null provider -- **local** - Mocks local provider -- **netapp-ontap** - Mocks NetApp ONTAP provider (for FSxN storage) - -## Benefits of Unit Tests - -✅ **Fast execution** - No actual resources created, tests complete in seconds -✅ **No AWS credentials required** - Mock providers eliminate need for authentication -✅ **Safe to run anywhere** - No risk of creating unexpected AWS resources -✅ **Comprehensive coverage** - Tests all conditional logic paths -✅ **Easy to debug** - Clear assertions with descriptive error messages -✅ **CI/CD friendly** - Can run in GitHub Actions without AWS access - -## Test Maintenance - -When modifying the Perforce module: - -1. **Adding new submodules** - Add test scenarios to `01_conditional_creation.tftest.hcl` -2. **Adding shared resources** - Add test scenarios to `02_shared_resources.tftest.hcl` -3. **Changing conditional logic** - Update assertions to match new behavior -4. **Adding required variables** - Update all test scenarios with new variables - -## Related Documentation - -- [Integration Tests](../integration/README.md) - Tests that deploy actual infrastructure -- [Module README](../../README.md) - Main module documentation -- [Terraform Testing Documentation](https://developer.hashicorp.com/terraform/language/tests) diff --git a/modules/perforce/variables.tf b/modules/perforce/variables.tf index d2e2809f..d2d9017b 100644 --- a/modules/perforce/variables.tf +++ b/modules/perforce/variables.tf @@ -44,8 +44,8 @@ variable "shared_lb_access_logs_bucket" { default = null # This should not be provided if access logging is disabled validation { - condition = var.enable_shared_lb_access_logs ? var.shared_lb_access_logs_bucket != null : true - error_message = "If access logging is disabled, the variable 'shared_lb_access_logs_bucket' must not be provided." + condition = var.enable_shared_lb_access_logs || var.shared_lb_access_logs_bucket == null + error_message = "If access logging is disabled, the variable 'shared_lb_access_logs_bucket' should not be provided." } } @@ -169,10 +169,10 @@ variable "route53_private_hosted_zone_name" { type = string description = "The name of the private Route53 Hosted Zone for the Perforce resources." default = null - # Should only be provided if create_route53_private_hosted_zone is set to true + # Must be provided if create_route53_private_hosted_zone is set to true validation { condition = var.create_route53_private_hosted_zone ? var.route53_private_hosted_zone_name != null : true - error_message = "If create_route53_private_hosted_zone is false, the variable 'route53_private_hosted_zone_name' must not be provided." + error_message = "If create_route53_private_hosted_zone is true, the variable 'route53_private_hosted_zone_name' must be provided." } } @@ -300,27 +300,27 @@ variable "p4_server_config" { default = null validation { - condition = length(var.p4_server_config.name) > 1 && length(var.p4_server_config.name) <= 50 - error_message = "The defined 'name' has too many characters (${length(var.p4_server_config.name)}). This can cause deployment failures for AWS resources with smaller character limits. Please reduce the character count and try again." + condition = var.p4_server_config == null || (length(var.p4_server_config.name) > 1 && length(var.p4_server_config.name) <= 50) + error_message = "The defined 'name' has too many characters. This can cause deployment failures for AWS resources with smaller character limits. Please reduce the character count and try again." } validation { - condition = var.p4_server_config.instance_architecture == "arm64" || var.p4_server_config.instance_architecture == "x86_64" + condition = var.p4_server_config == null || var.p4_server_config.instance_architecture == "arm64" || var.p4_server_config.instance_architecture == "x86_64" error_message = "The p4_server_config.instance_architecture variable must be either 'arm64' or 'x86_64'." } validation { - condition = contains(["p4d_commit", "p4d_replica"], var.p4_server_config.p4_server_type) - error_message = "${var.p4_server_config.p4_server_type} is not one of p4d_commit or p4d_replica." + condition = var.p4_server_config == null || contains(["p4d_commit", "p4d_replica"], var.p4_server_config.p4_server_type) + error_message = "The p4_server_config.p4_server_type must be one of p4d_commit or p4d_replica." } validation { - condition = contains(["EBS", "FSxN"], var.p4_server_config.storage_type) + condition = var.p4_server_config == null || contains(["EBS", "FSxN"], var.p4_server_config.storage_type) error_message = "Not a valid storage type. Valid values are either 'EBS' or 'FSxN'." } validation { - condition = !var.create_route53_private_hosted_zone || var.route53_private_hosted_zone_name == var.p4_server_config.fully_qualified_domain_name + condition = var.p4_server_config == null || !var.create_route53_private_hosted_zone || var.route53_private_hosted_zone_name == var.p4_server_config.fully_qualified_domain_name error_message = "Route53 zone name and Perforce Server FQDN must match." } @@ -548,6 +548,91 @@ variable "p4_code_review_config" { } +######################################## +# P4 Broker +######################################## +variable "p4_broker_config" { + type = object({ + # General + name = optional(string, "p4-broker") + project_prefix = optional(string, "cgd") + debug = optional(bool, false) + + # Compute + container_name = optional(string, "p4-broker-container") + container_port = optional(number, 1666) + container_cpu = optional(number, 1024) + container_memory = optional(number, 2048) + container_image = string + desired_count = optional(number, 1) + + # Broker Configuration + p4_target = string + broker_command_rules = optional(list(object({ + command = string + action = string + message = optional(string, null) + })), [{ command = "*", action = "pass", message = null }]) + extra_env = optional(map(string), null) + + # Storage & Logging + cloudwatch_log_retention_in_days = optional(number, 365) + + # Networking & Security + service_subnets = optional(list(string), null) + create_default_role = optional(bool, true) + custom_role = optional(string, null) + }) + + default = null + + description = <