From 3319040ad3f63b51882a7f75f9cdb46f11154535 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Wed, 11 Sep 2024 16:54:55 +0100 Subject: [PATCH 1/3] [NRL-863] Add stack version to env config --- .github/workflows/persistent-environment.yml | 5 +++ scripts/activate_stack.py | 10 ++++- scripts/set_env_config.py | 45 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100755 scripts/set_env_config.py diff --git a/.github/workflows/persistent-environment.yml b/.github/workflows/persistent-environment.yml index a8cdda85..0eb41845 100644 --- a/.github/workflows/persistent-environment.yml +++ b/.github/workflows/persistent-environment.yml @@ -232,6 +232,11 @@ jobs: - name: Terraform Apply run: terraform -chdir=terraform/infrastructure apply tfplan + - name: Update environment config version + run: | + deployed_version="${{ inputs.branch_name }}" + poetry run python ./scripts/set_env_config.py inactive-version ${deployed_version} ${{ inputs.environment }} + - name: Smoke Test run: | account=$(echo '${{ inputs.environment }}' | cut -d '-' -f1) diff --git a/scripts/activate_stack.py b/scripts/activate_stack.py index f7c1984c..46c9e8ee 100644 --- a/scripts/activate_stack.py +++ b/scripts/activate_stack.py @@ -8,7 +8,9 @@ CONFIG_LOCK_STATE = "lock-state" CONFIG_INACTIVE_STACK = "inactive-stack" +CONFIG_INACTIVE_VERSION = "inactive-version" CONFIG_ACTIVE_STACK = "active-stack" +CONFIG_ACTIVE_VERSION = "active-version" CONFIG_DOMAIN_NAME = "domain-name" STATE_LOCKED = "locked" @@ -16,6 +18,10 @@ VALID_LOCK_STATES = [STATE_LOCKED, STATE_OPEN] +def _swap_config(config: dict[str, str], key1: str, key2: str): + config[key1], config[key2] = config[key2], config[key1] + + def _set_lock_state( lock_state: str, environment_config: dict, parameters_key: str, sm: any ): @@ -134,8 +140,8 @@ def activate_stack(stack_name: str, env: str, session: any): sys.exit(1) print("Updating environment config and unlocking....") - environment_config[CONFIG_INACTIVE_STACK] = current_active_stack - environment_config[CONFIG_ACTIVE_STACK] = stack_name + _swap_config(environment_config, CONFIG_INACTIVE_STACK, CONFIG_ACTIVE_STACK) + _swap_config(environment_config, CONFIG_INACTIVE_VERSION, CONFIG_ACTIVE_VERSION) _update_and_unlock(environment_config, parameters_key=parameters_key, sm=sm) print(f"Complete. Stack {stack_name} is now the active stack for {env}") diff --git a/scripts/set_env_config.py b/scripts/set_env_config.py new file mode 100755 index 00000000..a63394ba --- /dev/null +++ b/scripts/set_env_config.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +import json +import sys + +import fire +from aws_session_assume import get_boto_session + + +def main(parameter_name: str, parameter_value: str, env: str): + boto_session = get_boto_session(env) + sm = boto_session.client("secretsmanager") + + secret_key = f"nhsd-nrlf--{env}--env-config" + response = sm.get_secret_value(SecretId=secret_key) + parameters = json.loads(response["SecretString"]) + + if parameter_name in parameters: + current_value = parameters[parameter_name] + if current_value == parameter_value: + print( + f"'{parameter_name}' already set to '{parameter_value}'. No changes made." + ) + sys.exit(0) + + print( + f"Updating '{parameter_name}' from '{current_value}' to '{parameter_value}'...." + ) + parameters[parameter_name] = parameter_value + else: + print(f"Adding '{parameter_name}' with value '{parameter_value}'....") + parameters[parameter_name] = parameter_value + + response = sm.put_secret_value( + SecretId=secret_key, SecretString=json.dumps(parameters) + ) + + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + print("Updated successfully") + else: + print(f"Update failed with: {response}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + fire.Fire(main) From f2641986584f625137b8b15537288a6988328a88 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Fri, 13 Sep 2024 14:32:40 +0100 Subject: [PATCH 2/3] [NRL-863] Fixup unit tests for activate_stack.py. Add more validation around environment config --- scripts/activate_stack.py | 27 ++++++++++++++++++- scripts/tests/test_activate_stack.py | 39 +++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/scripts/activate_stack.py b/scripts/activate_stack.py index 46c9e8ee..efbcad91 100644 --- a/scripts/activate_stack.py +++ b/scripts/activate_stack.py @@ -18,6 +18,31 @@ VALID_LOCK_STATES = [STATE_LOCKED, STATE_OPEN] +def _parse_env_config(raw_config: str) -> dict: + env_config = json.loads(raw_config) + if not all( + key in env_config + for key in [ + CONFIG_LOCK_STATE, + CONFIG_INACTIVE_STACK, + CONFIG_INACTIVE_VERSION, + CONFIG_ACTIVE_STACK, + CONFIG_ACTIVE_VERSION, + CONFIG_DOMAIN_NAME, + ] + ): + raise ValueError( + f"Environment config must contain keys: {CONFIG_LOCK_STATE}, {CONFIG_INACTIVE_STACK}, {CONFIG_INACTIVE_VERSION}, {CONFIG_ACTIVE_STACK}, {CONFIG_ACTIVE_VERSION}, {CONFIG_DOMAIN_NAME}" + ) + + if env_config[CONFIG_LOCK_STATE] not in VALID_LOCK_STATES: + raise ValueError( + f"Invalid lock state: {env_config[CONFIG_LOCK_STATE]}. Must be one of: {VALID_LOCK_STATES}" + ) + + return env_config + + def _swap_config(config: dict[str, str], key1: str, key2: str): config[key1], config[key2] = config[key2], config[key1] @@ -96,7 +121,7 @@ def activate_stack(stack_name: str, env: str, session: any): parameters_key = f"nhsd-nrlf--{env}--env-config" response = sm.get_secret_value(SecretId=parameters_key) - environment_config = json.loads(response["SecretString"]) + environment_config = _parse_env_config(response["SecretString"]) print(f"Got environment config for {env}: {environment_config}") current_active_stack = environment_config[CONFIG_ACTIVE_STACK] diff --git a/scripts/tests/test_activate_stack.py b/scripts/tests/test_activate_stack.py index a8cce01f..f401e81a 100644 --- a/scripts/tests/test_activate_stack.py +++ b/scripts/tests/test_activate_stack.py @@ -3,7 +3,7 @@ import pytest -from scripts.activate_stack import activate_stack +from scripts.activate_stack import _parse_env_config, activate_stack @pytest.fixture @@ -68,7 +68,9 @@ def _create_mock_env_config(): return { "lock-state": "open", "active-stack": "test-stack-1", + "active-version": "v1", "inactive-stack": "test-stack-2", + "inactive-version": "v2", "domain-name": "test.domain.name", } @@ -88,16 +90,17 @@ def test_happy_path(mock_boto_session, mock_secretsmanager): expected_env_config = { **mock_env_config, "active-stack": "test-stack-2", + "active-version": "v2", "inactive-stack": "test-stack-1", + "inactive-version": "v1", } assert result["SecretString"] == json.dumps(expected_env_config) def test_lock_state_not_open(mock_boto_session, mock_secretsmanager): inital_env_config = { + **_create_mock_env_config(), "lock-state": "locked", - "inactive-stack": "test-stack-1", - "active-stack": "test-stack-2", } mock_secretsmanager.create_secret( Name="nhsd-nrlf--locked--env-config", SecretString=json.dumps(inital_env_config) @@ -115,7 +118,7 @@ def test_lock_state_not_open(mock_boto_session, mock_secretsmanager): def test_stack_already_active(mock_boto_session, mock_secretsmanager): intial_env_config = { - "lock-state": "open", + **_create_mock_env_config(), "inactive-stack": "test-stack-1", "active-stack": "test-stack-2", } @@ -132,3 +135,31 @@ def test_stack_already_active(mock_boto_session, mock_secretsmanager): SecretId="nhsd-nrlf--already-active--env-config" # pragma: allowlist secret ) assert result["SecretString"] == json.dumps(intial_env_config) + + +def test_parse_env_config_valid(): + valid_config = json.dumps(_create_mock_env_config()) + result = _parse_env_config(valid_config) + assert result == _create_mock_env_config() + + +def test_parse_env_config_empty(): + with pytest.raises(ValueError): + _parse_env_config("") + + +def test_parse_env_config_invalid_json(): + with pytest.raises(ValueError): + _parse_env_config("this is not JSON!") + + +def test_parse_env_config_missing_params(): + with pytest.raises(ValueError): + _parse_env_config(json.dumps({"lock-state": "open"})) + + +def test_parse_env_config_invalid_lock_state(): + with pytest.raises(ValueError): + _parse_env_config( + json.dumps({**_create_mock_env_config(), "lock-state": "invalid"}) + ) From f099abac0c753536b52862d4a4ea6e5d965f17c5 Mon Sep 17 00:00:00 2001 From: Matt Dean Date: Tue, 17 Sep 2024 09:08:13 +0100 Subject: [PATCH 3/3] [NRL-863] Add short commit ref to versions in env config --- .github/workflows/persistent-environment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/persistent-environment.yml b/.github/workflows/persistent-environment.yml index 0eb41845..a3333620 100644 --- a/.github/workflows/persistent-environment.yml +++ b/.github/workflows/persistent-environment.yml @@ -234,7 +234,8 @@ jobs: - name: Update environment config version run: | - deployed_version="${{ inputs.branch_name }}" + short_commit_ref="$(echo ${{ github.sha }} | cut -c1-8)" + deployed_version="${{ inputs.branch_name }}@${short_commit_ref}" poetry run python ./scripts/set_env_config.py inactive-version ${deployed_version} ${{ inputs.environment }} - name: Smoke Test