Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/multi-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ terraform output -raw webhook_secret

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.4.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 6.33 |
| <a name="requirement_local"></a> [local](#requirement\_local) | ~> 2.0 |
| <a name="requirement_random"></a> [random](#requirement\_random) | ~> 3.0 |
Expand Down
2 changes: 1 addition & 1 deletion examples/multi-runner/versions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ terraform {
version = "~> 3.0"
}
}
required_version = ">= 1.3.0"
required_version = ">= 1.4.0"
}
15 changes: 10 additions & 5 deletions modules/multi-runner/README.md

Large diffs are not rendered by default.

100 changes: 76 additions & 24 deletions modules/multi-runner/tests/config-resolution.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,9 @@ mock_provider "random" {}
mock_provider "null" {}

variables {
aws_region = "eu-west-1"
vpc_id = "vpc-stable"
subnet_ids = ["subnet-stable"]

github_app = {
key_base64_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/key"
name = "/tests/github-app/key"
}
id_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/id"
name = "/tests/github-app/id"
}
webhook_secret_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/webhook-secret"
name = "/tests/github-app/webhook-secret"
}
}

lambda_s3_bucket = "test-lambda-artifacts"
runners_lambda_zip = "README.md"
runners_lambda_s3_key = "runners.zip"
webhook_lambda_s3_key = "webhook.zip"
syncer_lambda_s3_key = "runner-binaries-syncer.zip"
aws_region = "eu-west-1"
prefix = "test"
aws_partition = "aws"

experimental_global_config_github = {
app = {
Expand Down Expand Up @@ -152,6 +131,30 @@ run "v1_stable_inputs_translate_into_effective_base" {
command = plan

variables {
vpc_id = "vpc-stable"
subnet_ids = ["subnet-stable"]

github_app = {
key_base64_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/key"
name = "/tests/github-app/key"
}
id_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/id"
name = "/tests/github-app/id"
}
webhook_secret_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/webhook-secret"
name = "/tests/github-app/webhook-secret"
}
}

lambda_s3_bucket = "test-lambda-artifacts"
runners_lambda_zip = "README.md"
runners_lambda_s3_key = "runners.zip"
webhook_lambda_s3_key = "webhook.zip"
syncer_lambda_s3_key = "runner-binaries-syncer.zip"

tags = {
source = "v1"
}
Expand Down Expand Up @@ -408,3 +411,52 @@ run "v2_experimental_inputs_resolve_lane_over_global" {
error_message = "Experimental v2 configurations must route through module.runner_configs and skip the legacy runners module."
}
}

run "v2_inputs_do_not_require_legacy_arguments" {
command = plan

variables {
experimental_global_config_compute_provider = {
aws = {
ec2 = {
vpc_id = "vpc-v2"
subnet_ids = ["subnet-v2"]
runner_binaries = {
enabled = false
}
}
}
}
experimental_multi_runner_config = {
lane = {
orchestration_provider = {
webhook = {
matcherConfig = {
labelMatchers = [["self-hosted", "linux", "x64"]]
}
}
}
compute_provider = {
aws = {
ec2 = {
instance_types = ["m5.large"]
binaries_syncer = {
enabled = false
}
}
}
}
}
}
}

assert {
condition = (
local.use_v2_config
&& keys(module.runner_configs) == ["lane"]
&& length(module.runners) == 0
&& local.resolved_config.multi_runner_config["lane"].compute_provider.aws.ec2.vpc_id == "vpc-v2"
)
error_message = "The v2 interface must work without the stable v1 GitHub App, VPC, subnet, or runner configuration inputs."
}
}
82 changes: 82 additions & 0 deletions modules/multi-runner/validations.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
locals {
common_validation_errors = concat(
alltrue([
for app in var.additional_github_apps :
(app.key_base64 != null || app.key_base64_ssm != null) &&
(app.id != null || app.id_ssm != null)
]) ? [] : ["Each additional GitHub app must provide either key_base64 or key_base64_ssm, and either id or id_ssm."],
contains(["STANDARD", "INFREQUENT_ACCESS"], var.log_class) ? [] : ["`log_class` must be either `STANDARD` or `INFREQUENT_ACCESS`."],
contains(["first", "random", "all"], var.queue_selection_strategy) ? [] : ["`queue_selection_strategy` value not valid. Valid values are 'first', 'random', 'all'."],
contains(["silly", "trace", "debug", "info", "warn", "error", "fatal"], var.log_level) ? [] : ["`log_level` value not valid. Valid values are 'silly', 'trace', 'debug', 'info', 'warn', 'error', 'fatal'."],
contains(["arm64", "x86_64"], var.lambda_architecture) ? [] : ["`lambda_architecture` value is not valid, valid values are: `arm64` and `x86_64`."],
contains(["ENABLED", "DISABLED", "ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS"], var.state_event_rule_binaries_syncer) ? [] : ["`state_event_rule_binaries_syncer` value is not valid, valid values are: `ENABLED`, `DISABLED`, `ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS`."],
var.queue_encryption == null || var.queue_encryption.sqs_managed_sse_enabled != null && var.queue_encryption.kms_master_key_id == null && var.queue_encryption.kms_data_key_reuse_period_seconds == null || var.queue_encryption.sqs_managed_sse_enabled == null && var.queue_encryption.kms_master_key_id != null ? [] : ["Invalid configuration for `queue_encryption`. Valid configurations are encryption disabled, enabled via SSE. Or encryption via KMS."],
contains(["Standard", "Advanced"], var.matcher_config_parameter_store_tier) ? [] : ["`matcher_config_parameter_store_tier` value is not valid, valid values are: `Standard`, and `Advanced`."],
!var.iam_overrides.override_instance_profile || var.iam_overrides.instance_profile_name != null ? [] : ["instance_profile_name must be provided when override_instance_profile is true."],
!var.iam_overrides.override_runner_role || var.iam_overrides.runner_role_arn != null ? [] : ["runner_role_arn must be provided when override_runner_role is true."]
)
}

resource "terraform_data" "validate_v1" {
count = local.use_v2_config ? 0 : 1

lifecycle {
precondition {
condition = length(local.common_validation_errors) == 0
error_message = join("\n", local.common_validation_errors)
}

precondition {
condition = (
(var.github_app.key_base64 != null || var.github_app.key_base64_ssm != null) &&
(var.github_app.id != null || var.github_app.id_ssm != null) &&
(var.github_app.webhook_secret != null || var.github_app.webhook_secret_ssm != null) &&
var.vpc_id != null &&
var.subnet_ids != null &&
length(var.multi_runner_config) > 0
)
error_message = "Stable v1 configuration requires github_app, vpc_id, subnet_ids, and multi_runner_config."
}
}
}

resource "terraform_data" "validate_v2" {
count = local.use_v2_config ? 1 : 0

lifecycle {
precondition {
condition = length(local.common_validation_errors) == 0
error_message = join("\n", local.common_validation_errors)
}

precondition {
condition = (
(
try(var.experimental_global_config_github.app.key_base64, null) != null ||
try(var.experimental_global_config_github.app.key_base64_ssm, null) != null
) && (
try(var.experimental_global_config_github.app.id, null) != null ||
try(var.experimental_global_config_github.app.id_ssm, null) != null
) && (
try(var.experimental_global_config_github.app.webhook_secret, null) != null ||
try(var.experimental_global_config_github.app.webhook_secret_ssm, null) != null
)
)
error_message = "Experimental v2 configuration requires a complete GitHub App under experimental_global_config_github.app."
}

precondition {
condition = alltrue([
for config in local.resolved_config.multi_runner_config : (
try(config.orchestration_provider.webhook != null, false) &&
try(length(config.orchestration_provider.webhook.matcherConfig.labelMatchers) > 0, false) &&
try(config.compute_provider.aws.ec2 != null, false) &&
try(length(config.compute_provider.aws.ec2.instance_types) > 0, false) &&
try(config.compute_provider.aws.ec2.vpc_id != null, false) &&
try(length(config.compute_provider.aws.ec2.subnet_ids) > 0, false)
)
])
error_message = "Each experimental v2 runner lane requires a webhook matcher, EC2 instance_types, vpc_id, and at least one subnet."
}
}
}
78 changes: 9 additions & 69 deletions modules/multi-runner/variables.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
variable "github_app" {
description = <<EOF
GitHub app parameters, see your github app.
GitHub app parameters for the stable v1 interface, see your github app.
Omit this value when using the experimental v2 interface and provide the
app through `experimental_global_config_github` instead.
You can optionally create the SSM parameters yourself and provide the ARN and name here, through the `*_ssm` attributes.
If you chose to provide the configuration values directly here,
please ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`).
Expand All @@ -23,16 +25,7 @@ variable "github_app" {
name = string
}))
})

validation {
condition = (var.github_app.key_base64 != null || var.github_app.key_base64_ssm != null) && (var.github_app.id != null || var.github_app.id_ssm != null) && (var.github_app.webhook_secret != null || var.github_app.webhook_secret_ssm != null)
error_message = <<EOF
You must set all of the following parameters, choosing one option from each pair:
- `key_base64` or `key_base64_ssm`
- `id` or `id_ssm`
- `webhook_secret` or `webhook_secret_ssm`
EOF
}
default = {}
}


Expand All @@ -58,14 +51,6 @@ variable "additional_github_apps" {
installation_id_ssm = optional(object({ arn = string, name = string }))
}))
default = []
validation {
condition = alltrue([
for app in var.additional_github_apps :
(app.key_base64 != null || app.key_base64_ssm != null) &&
(app.id != null || app.id_ssm != null)
])
error_message = "Each additional GitHub app must provide either key_base64 or key_base64_ssm, and either id or id_ssm."
}
}

variable "prefix" {
Expand Down Expand Up @@ -250,6 +235,7 @@ variable "multi_runner_config" {
maxReceiveCount = null
})
}))
default = {}
description = <<EOT
multi_runner_config = {
runner_config: {
Expand Down Expand Up @@ -396,11 +382,6 @@ variable "log_class" {
description = "The log class of the CloudWatch log groups. Valid values are `STANDARD` or `INFREQUENT_ACCESS`."
type = string
default = "STANDARD"

validation {
condition = contains(["STANDARD", "INFREQUENT_ACCESS"], var.log_class)
error_message = "`log_class` must be either `STANDARD` or `INFREQUENT_ACCESS`."
}
}

variable "lambda_s3_bucket" {
Expand Down Expand Up @@ -440,28 +421,12 @@ variable "queue_selection_strategy" {
description = "Strategy used to pick a queue when multiple runner configurations match a job equally well. `first` keeps the historical deterministic behaviour (the first matching queue by priority). `random` spreads jobs across the matching queues to avoid concentrating load on a single one. `all` scales up one runner per matching queue and lets the first to become available take the job (favouring speed over cost; this multiplies instance launches and runner registrations per job)."
type = string
default = "first"
validation {
condition = contains(["first", "random", "all"], var.queue_selection_strategy)
error_message = "`queue_selection_strategy` value not valid. Valid values are 'first', 'random', 'all'."
}
}

variable "log_level" {
description = "Logging level for lambda logging. Valid values are 'silly', 'trace', 'debug', 'info', 'warn', 'error', 'fatal'."
type = string
default = "info"
validation {
condition = anytrue([
var.log_level == "silly",
var.log_level == "trace",
var.log_level == "debug",
var.log_level == "info",
var.log_level == "warn",
var.log_level == "error",
var.log_level == "fatal",
])
error_message = "`log_level` value not valid. Valid values are 'silly', 'trace', 'debug', 'info', 'warn', 'error', 'fatal'."
}
}

variable "lambda_runtime" {
Expand All @@ -474,10 +439,6 @@ variable "lambda_architecture" {
description = "AWS Lambda architecture. Lambda functions using Graviton processors ('arm64') tend to have better price/performance than 'x86_64' functions. "
type = string
default = "arm64"
validation {
condition = contains(["arm64", "x86_64"], var.lambda_architecture)
error_message = "`lambda_architecture` value is not valid, valid values are: `arm64` and `x86_64`."
}
}

variable "syncer_lambda_s3_key" {
Expand Down Expand Up @@ -547,11 +508,6 @@ variable "state_event_rule_binaries_syncer" {
type = string
description = "Option to disable EventBridge Lambda trigger for the binary syncer, useful to stop automatic updates of binary distribution"
default = "ENABLED"

validation {
condition = contains(["ENABLED", "DISABLED", "ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS"], var.state_event_rule_binaries_syncer)
error_message = "`state_event_rule_binaries_syncer` value is not valid, valid values are: `ENABLED`, `DISABLED`, `ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS`."
}
}

variable "queue_encryption" {
Expand All @@ -566,10 +522,6 @@ variable "queue_encryption" {
kms_master_key_id = null
sqs_managed_sse_enabled = true
}
validation {
condition = var.queue_encryption == null || var.queue_encryption.sqs_managed_sse_enabled != null && var.queue_encryption.kms_master_key_id == null && var.queue_encryption.kms_data_key_reuse_period_seconds == null || var.queue_encryption.sqs_managed_sse_enabled == null && var.queue_encryption.kms_master_key_id != null
error_message = "Invalid configuration for `queue_encryption`. Valid configurations are encryption disabled, enabled via SSE. Or encryption via KMS."
}
}

variable "aws_partition" {
Expand All @@ -584,13 +536,15 @@ variable "aws_region" {
}

variable "vpc_id" {
description = "The VPC for security groups of the action runners."
description = "The VPC for security groups of stable v1 action runners. Omit when using the experimental v2 interface."
type = string
default = null
}

variable "subnet_ids" {
description = "List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`."
description = "List of subnets in which stable v1 action runners will be launched. Omit when using the experimental v2 interface."
type = list(string)
default = null
}

variable "enable_managed_runner_security_group" {
Expand Down Expand Up @@ -796,10 +750,6 @@ variable "matcher_config_parameter_store_tier" {
description = "The tier of the parameter store for the matcher configuration. Valid values are `Standard`, and `Advanced`."
type = string
default = "Standard"
validation {
condition = contains(["Standard", "Advanced"], var.matcher_config_parameter_store_tier)
error_message = "`matcher_config_parameter_store_tier` value is not valid, valid values are: `Standard`, and `Advanced`."
}
}

variable "metrics" {
Expand Down Expand Up @@ -847,16 +797,6 @@ variable "iam_overrides" {
override_runner_role = false
runner_role_arn = null
}

validation {
condition = !var.iam_overrides.override_instance_profile || var.iam_overrides.instance_profile_name != null
error_message = "instance_profile_name must be provided when override_instance_profile is true."
}

validation {
condition = !var.iam_overrides.override_runner_role || var.iam_overrides.runner_role_arn != null
error_message = "runner_role_arn must be provided when override_runner_role is true."
}
}

variable "lambda_event_source_mapping_batch_size" {
Expand Down
2 changes: 1 addition & 1 deletion modules/multi-runner/versions.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
terraform {
required_version = ">= 1.3"
required_version = ">= 1.4"

required_providers {
aws = {
Expand Down
Loading