Skip to content
Merged
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
63 changes: 63 additions & 0 deletions .claude/guidelines/testing_for_tfvars_changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Testing for `terraform.tfvars` Variable Changes

## Scope

Applies when a PR adds, modifies, or removes a top-level `terraform.tfvars` variable that drives any generated artefact under [`assets/src/`](../../assets/src/) — `tower.yml`, `tower.env`, `docker-compose.yml`, `tower.sql`, Ansible files, Wave-Lite config, etc.

The framework only catches drift on assertions that exist. Hardcoded values in templates are silent test gaps until they become parameters; this protocol forces the gap to be filled at PR time. Background on the framework: [`testcase_architecture.md`](../../tests/testcase_architecture.md).

## Required steps

### 1. Update both baseline assertions

In [`tests/datafiles/expected_results/expected_results.py`](../../tests/datafiles/expected_results/expected_results.py), add the resulting key/value to BOTH `generate_<file>_entries_all_active` AND `generate_<file>_entries_all_disabled` for every artefact the variable affects.

Use the file-type-appropriate key syntax:

| File type | Key syntax | Example |
| --------- | ---------- | ------- |
| `*.yml` | YAMLPath (dot-separated) | `tower.participant.auto-create-user` |
| `*.env` | Plain key | `TOWER_DB_URL` |
| `*.sql` | Full-page compare via `tests/datafiles/expected_results/expected_sql/<file>.sql` | n/a |

For a binary feature flag, the two baselines should typically assert opposite values (`_all_active` = on, `_all_disabled` = off). For non-flag variables, both baselines may assert the same default.

### 2. Set the `_all_active`-side default in `base-overrides.auto.tfvars`

This is the project's preferred pattern: every test inherits the "all enabled" value by default, and `_all_disabled` tests list the explicit inverse overrides (step 3). The result is that `_all_active`-flavoured tests need minimal `tf_modifiers`, and the inversion is concentrated in `_all_disabled` tests where it's already idiomatic.

Edit the heredoc in [`tests/datafiles/generate_core_data.sh`](../../tests/datafiles/generate_core_data.sh) — add the variable to the appropriate section with its `_all_active` value. The next test run regenerates `tests/datafiles/base-overrides.auto.tfvars`, which is auto-loaded by `terraform plan` for every test.

**Don't conflate test convenience with deployment policy**: this does NOT change [`templates/TEMPLATE_terraform.tfvars`](../../templates/TEMPLATE_terraform.tfvars). The shipped default for new deployers stays untouched. Flipping the TEMPLATE default is a separate decision requiring CHANGELOG amendment per [`changelog_protocol.md`](changelog_protocol.md).

**Cache invalidation note**: changing `base-overrides.auto.tfvars` invalidates all cached terraform plans. The rebuild is automatic but takes minutes on first re-run.

**Exception — when NOT to use this approach**: if the variable is only relevant to one specific scenario (e.g., a private-CA test) rather than the general "all enabled" baseline, set it via per-test `tf_modifiers` in [`test_config_file_content.py`](../../tests/unit/config_files/test_config_file_content.py) instead. Default to base-overrides; fall back to per-test only when the variable is genuinely scenario-local.

### 3. Add inverse overrides to every `_all_disabled`-style test

Every test that calls `generate_assertions_all_disabled` must explicitly override the new flag(s) to the `_all_disabled` value in its `tf_modifiers`. Without this, those tests silently pass in default mode (the new key isn't in their narrow `desired_files`) but **fail under `TEST_FULL=true`**.

Find them with:

```bash
grep -n "generate_assertions_all_disabled" tests/unit/config_files/test_config_file_content.py
```

**Don't rely on test naming alone** — some tests like `test_seqera_hosted_wave_active` call `generate_assertions_all_disabled` despite an "active"-flavoured suffix.

### 4. Verify with `TEST_FULL=true` (required before merge)

Default `make run_tests` only validates files listed in each test's `desired_files`. A test can pass while leaving the new key entirely unvalidated, because the file containing it was never generated. **The PR is not solid until `TEST_FULL=true` passes.**

```bash
TEST_FULL=true pytest tests/unit/config_files
```

If `TEST_FULL` surfaces failures unrelated to your change, surface them to the user — don't silently chase them as part of this PR.

## Don't

- **Don't add a new testcase without explicit user authorization.** The two baselines (`_all_active` and `_all_disabled`) are usually sufficient for boolean flags. If you believe a new scenario test is needed, propose it to the user with justification — explain why the existing baselines can't cover the case (e.g., the flag interacts non-obviously with another flag, or it changes resource topology in a way that needs targeted assertion). Wait for sign-off before writing the test.
- **Don't change `templates/TEMPLATE_terraform.tfvars` defaults to simplify tests.** That's a deployment-policy change requiring CHANGELOG amendment and stakeholder review per [`changelog_protocol.md`](changelog_protocol.md).
- **Don't trust default-mode passing tests as proof of correctness** for assertions on files not in `desired_files`. Confirm with `TEST_FULL=true`.
3 changes: 3 additions & 0 deletions 009_define_file_templates.tf
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ locals {

tower_audit_retention_days = var.tower_audit_retention_days,

flag_tower_enable_participant_auto_create_user = var.flag_tower_enable_participant_auto_create_user,
flag_tower_enable_member_auto_create_user = var.flag_tower_enable_member_auto_create_user,

flag_using_micronaut_4 = local.flag_using_micronaut_4,

}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ $ git log origin/master..origin/gwright99/25_2_0_update --oneline
- Documentation
- Added Studios SSH design decision to `design_decisions.md`. [`#313`](https://github.com/seqeralabs/cx-field-tools-installer/issues/313)
- Updated `TEMPLATE_terraform.tfvars` with Studios SSH variables and configuration guidance. [`#313`](https://github.com/seqeralabs/cx-field-tools-installer/issues/313)
- Added explicit Tower user auto-creation flags (`flag_tower_enable_participant_auto_create_user`, `flag_tower_enable_member_auto_create_user`) to `TEMPLATE_terraform.tfvars`. [`#312`](https://github.com/seqeralabs/cx-field-tools-installer/issues/312)
- TBD
<br /><br />

Expand All @@ -47,6 +48,9 @@ $ git log origin/master..origin/gwright99/25_2_0_update --oneline
| New | Studios SSH | `flag_limit_data_studio_ssh_to_some_workspaces` | When `true`, restricts SSH access to the workspace IDs listed in `data_studio_ssh_eligible_workspaces`. When `false`, SSH is available to all workspaces. |
| New | Studios SSH | `data_studio_ssh_eligible_workspaces` | Comma-separated list of numeric workspace IDs that are permitted to use Studios SSH. Only evaluated when `flag_limit_data_studio_ssh_to_some_workspaces = true`. |
||||
| New | Tower Auth | `flag_tower_enable_participant_auto_create_user` | Controls Tower's `tower.participant.auto-create-user`. When `true`, allows a workspace participant to be auto-created when added by email. Defaults to `false`. [`#312`](https://github.com/seqeralabs/cx-field-tools-installer/issues/312) |
| New | Tower Auth | `flag_tower_enable_member_auto_create_user` | Controls Tower's `tower.member.auto-create-user`. When `true`, allows the underlying User entity to be auto-created when an email is added as an Org Member. Defaults to `false`. [`#312`](https://github.com/seqeralabs/cx-field-tools-installer/issues/312) |
||||
| Modified | Studios | `data_studio_container_version` | Updated from 0.9.0 to 0.11.0. Note: Studios SSH requires connect-proxy >= 0.10.0. |
| Modified | Studios | `data_studio_options` | Removed 0.9.0 images and added 0.11.0 images. |
||||
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ References in the table below use these prefixes:
| Writing or editing Terraform (`*.tf`, `terraform.tfvars`) | `proj:terraform_conventions.md`, `proj:terraform_style_guide.md` |
| Writing or editing Python | `proj:python_standards.md` |
| Running, writing, or analysing tests | `proj:testing_strategy.md`, `proj:testing_commands.md` |
| Adding/modifying a `terraform.tfvars` variable that drives a generated artefact (`tower.yml`, `tower.env`, etc.) | `proj:testing_for_tfvars_changes.md` |
| Working on security-sensitive code (secrets, IAM, SSM, certificates) | `proj:security_considerations.md` |

## Project Overview
Expand Down
15 changes: 11 additions & 4 deletions assets/src/tower_config/tower.yml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,19 @@ tower:
- label: "Docs"
url: "https://docs.seqera.io"

# Controls whether an individual must have a registered account with Seqera Platform before they can be
# Controls whether an individual must have a registered account with Seqera Platform before they can be
# added directly to a Workspace as a collaborator.
# - If `true`: The user does not need a pre-existing Platform account before they can be added.
# - If `false`: The user must have a pre-existing Platform account before they can be added.
# - If `true`: The user does not need a pre-existing Platform account before they can be added.
# - If `false`: The user must have a pre-existing Platform account before they can be added.
participant:
auto-create-user: false
auto-create-user: ${flag_tower_enable_participant_auto_create_user}

# Controls whether an individual must have a registered account with Seqera Platform before they can be
# added directly to an Organization as a member.
# - If `true`: The user does not need a pre-existing Platform account before they can be added.
# - If `false`: The user must have a pre-existing Platform account before they can be added.
member:
auto-create-user: ${flag_tower_enable_member_auto_create_user}

# By default, if an update is not received for 180 seconds, Seqera Platform marks the pipeline status as UNKNOWN.
# Making this setting explicit in case some sites need to customize due to local conditions.
Expand Down
14 changes: 14 additions & 0 deletions templates/TEMPLATE_terraform.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,20 @@ tower_root_users = "REPLACE_ME"
tower_email_trusted_orgs = "REPLACE_ME"
tower_email_trusted_users = "REPLACE_ME"

# Controls whether Seqera Platform will auto-create an underlying User entity when an
# unknown email is added directly to a resource.
#
# - `flag_tower_enable_participant_auto_create_user` governs Workspace **collaborators**
# (`tower.participant.auto-create-user`). If `true`, the email does not need a
# pre-existing Platform account; if `false`, the User must already exist.
#
# - `flag_tower_enable_member_auto_create_user` governs Organization **members**
# (`tower.member.auto-create-user`). If `true`, adding an unknown email as an
# Organization User auto-generates the underlying Platform User; if `false`, the
# User must already exist.
flag_tower_enable_participant_auto_create_user = false
flag_tower_enable_member_auto_create_user = false

tower_audit_retention_days = 1095 # 3 years (value in days)

tower_enable_openapi = true
Expand Down
2 changes: 1 addition & 1 deletion tests/datafiles/012_testing_outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
## ------------------------------------------------------------------------------------
output local_wave_enabled {
value = local.wave_enabled
}
}
48 changes: 36 additions & 12 deletions tests/datafiles/expected_results/expected_results.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from tests.utils.config import all_template_files, expected_sql_dir
from tests.utils.filehandling import FileHelper


"""
Generate two sets of baseline value sets.
1. Settings when all features / assets activated.
Expand Down Expand Up @@ -133,6 +134,8 @@ def generate_tower_yml_entries_all_active(overrides={}):
"micronaut.application.name": "tower-testing",
"tower.cron.audit-log.clean-up.time-offset": "1095d",
"tower.data-studio.allowed-workspaces": None,
"tower.member.auto-create-user": True,
"tower.participant.auto-create-user": True,
"tower.trustedEmails[0]": "'graham.wright@seqera.io,gwright99@hotmail.com'",
"tower.trustedEmails[1]": "'*@abc.com,*@def.com'",
"tower.trustedEmails[2]": "'123@abc.com,456@def.com'",
Expand Down Expand Up @@ -244,12 +247,22 @@ def generate_assertions_all_active(template_files, overrides):
entries = {
"tower_env": generate_tower_env_entries_all_active(overrides["tower_env"]),
"tower_yml": generate_tower_yml_entries_all_active(overrides["tower_yml"]),
"data_studios_env": generate_data_studios_env_entries_all_active(overrides["data_studios_env"]),
"data_studios_env": generate_data_studios_env_entries_all_active(
overrides["data_studios_env"]
),
"tower_sql": generate_tower_sql_entries_all_active(overrides["tower_sql"]),
"docker_compose": generate_docker_compose_yml_entries_all_active(overrides["docker_compose"]),
"wave_lite_yml": generate_wave_lite_yml_entries_all_active(overrides["wave_lite_yml"]),
"wave_lite_rds": generate_wave_lite_rds_entries_all_active(overrides["wave_lite_rds"]),
"groundswell_env": generate_groundswell_env_entries_all_active(overrides["groundswell_env"]),
"docker_compose": generate_docker_compose_yml_entries_all_active(
overrides["docker_compose"]
),
"wave_lite_yml": generate_wave_lite_yml_entries_all_active(
overrides["wave_lite_yml"]
),
"wave_lite_rds": generate_wave_lite_rds_entries_all_active(
overrides["wave_lite_rds"]
),
"groundswell_env": generate_groundswell_env_entries_all_active(
overrides["groundswell_env"]
),
# TODO: Build out stubs OR identify as not-in-scope due to other testing method (e.g. .sql)
"groundswell_sql": {"present": {}, "omitted": {}},
"seqerakit_yml": {"present": {}, "omitted": {}},
Expand Down Expand Up @@ -391,6 +404,8 @@ def generate_tower_yml_entries_all_disabled(overrides={}):
"mail.smtp.ssl.protocols": "TLSv1.2",
"micronaut.application.name": "tower-testing",
"tower.cron.audit-log.clean-up.time-offset": "1095d",
"tower.member.auto-create-user": False,
"tower.participant.auto-create-user": False,
"tower.trustedEmails[0]": "'graham.wright@seqera.io,gwright99@hotmail.com'",
"tower.trustedEmails[1]": "'*@abc.com,*@def.com'",
"tower.trustedEmails[2]": "'123@abc.com,456@def.com'",
Expand Down Expand Up @@ -499,12 +514,22 @@ def generate_assertions_all_disabled(template_files, overrides):
entries = {
"tower_env": generate_tower_env_entries_all_disabled(overrides["tower_env"]),
"tower_yml": generate_tower_yml_entries_all_disabled(overrides["tower_yml"]),
"data_studios_env": generate_data_studios_env_entries_all_disabled(overrides["data_studios_env"]),
"data_studios_env": generate_data_studios_env_entries_all_disabled(
overrides["data_studios_env"]
),
"tower_sql": generate_tower_sql_entries_all_disabled(overrides["tower_sql"]),
"docker_compose": generate_docker_compose_yml_entries_all_disabled(overrides["docker_compose"]),
"wave_lite_yml": generate_wave_lite_yml_entries_all_disabled(overrides["wave_lite_yml"]),
"wave_lite_rds": generate_wave_lite_rds_entries_all_disabled(overrides["wave_lite_rds"]),
"groundswell_env": generate_groundswell_env_entries_all_disabled(overrides["groundswell_env"]),
"docker_compose": generate_docker_compose_yml_entries_all_disabled(
overrides["docker_compose"]
),
"wave_lite_yml": generate_wave_lite_yml_entries_all_disabled(
overrides["wave_lite_yml"]
),
"wave_lite_rds": generate_wave_lite_rds_entries_all_disabled(
overrides["wave_lite_rds"]
),
"groundswell_env": generate_groundswell_env_entries_all_disabled(
overrides["groundswell_env"]
),
# TODO: Build out stubs OR identify as not-in-scope due to other testing method (e.g. .sql)
"groundswell_sql": {"present": {}, "omitted": {}},
"seqerakit_yml": {"present": {}, "omitted": {}},
Expand Down Expand Up @@ -542,8 +567,7 @@ def purge_baseline_of_specified_overrides(baseline, overrides):


def assertion_modifiers_template():
"""
Generate a blank dict for each testcase file to attach test-specific assertion modifiers to.
"""Generate a blank dict for each testcase file to attach test-specific assertion modifiers to.
These are then reconciled with the core set of assertions in `expected_results.py` (via `generate_assertions_xxx`).
"""
return {k: {} for k in all_template_files}
21 changes: 12 additions & 9 deletions tests/datafiles/generate_core_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ tower_root_users = "graham.wright@seqera.io,gwright99@hotmail.com"
tower_email_trusted_orgs = "*@abc.com, *@def.com"
tower_email_trusted_users = "123@abc.com, 456@def.com"

flag_tower_enable_participant_auto_create_user = true
flag_tower_enable_member_auto_create_user = true

tower_enable_openapi = true

tower_enable_pipeline_versioning = true
Expand Down Expand Up @@ -266,22 +269,22 @@ python3 generate_testing_secrets.py
update_ssm_parameter() {
local param_name="$1"
local local_file="$2"

echo "Processing parameter: $param_name"

# Try to get current value (single call)
current_value=$(aws ssm get-parameter --name "$param_name" --with-decryption --query 'Parameter.Value' --output text 2>/dev/null)

if [ $? -eq 0 ]; then
echo "Parameter $param_name exists, checking if update is needed..."

# Hash the current value
current_hash=$(echo -n "$current_value" | sha256sum | cut -d' ' -f1)

# Get local file content and hash it
local_content=$(cat "$local_file")
local_hash=$(echo -n "$local_content" | sha256sum | cut -d' ' -f1)

# Compare hashes
if [ "$current_hash" = "$local_hash" ]; then
echo "Parameter $param_name is up to date (hash match)"
Expand All @@ -292,14 +295,14 @@ update_ssm_parameter() {
else
echo "Parameter $param_name does not exist, creating..."
fi

# Create or update parameter
aws ssm put-parameter \
--name "$param_name" \
--value "$(cat "$local_file")" \
--type "SecureString" \
--overwrite

echo "Parameter $param_name updated successfully"
}

Expand Down Expand Up @@ -331,5 +334,5 @@ cat << 'EOF' > 012_testing_outputs.tf
## ------------------------------------------------------------------------------------
output local_wave_enabled {
value = local.wave_enabled
}
}
EOF
Loading
Loading