diff --git a/src/envars/main.py b/src/envars/main.py index 04a8685..952e23e 100644 --- a/src/envars/main.py +++ b/src/envars/main.py @@ -414,27 +414,33 @@ def _get_resolved_variables( # Parameter Store and Secret Manager substitution if manager.cloud_provider == "aws": - ssm_store = SSMParameterStore() - cf_exports = CloudFormationExports() + ssm_store = None + cf_exports = None for var_name, value in resolved_vars.items(): if isinstance(value, str): if value.startswith("parameter_store:"): + if ssm_store is None: + ssm_store = SSMParameterStore() param_name = value.split(":", 1)[1] param_value = ssm_store.get_parameter(param_name) if param_value is None: raise ValueError(f"Parameter '{param_name}' not found in Parameter Store.") resolved_vars[var_name] = param_value elif value.startswith("cloudformation_export:"): + if cf_exports is None: + cf_exports = CloudFormationExports() export_name = value.split(":", 1)[1] export_value = cf_exports.get_export_value(export_name) if export_value is None: raise ValueError(f"Export '{export_name}' not found in CloudFormation exports.") resolved_vars[var_name] = export_value elif manager.cloud_provider == "gcp": - gcp_secret_manager = GCPSecretManager() + gcp_secret_manager = None for var_name, value in resolved_vars.items(): if isinstance(value, str): if value.startswith("gcp_secret_manager:"): + if gcp_secret_manager is None: + gcp_secret_manager = GCPSecretManager() secret_name = value.split(":", 1)[1] secret_value = gcp_secret_manager.access_secret_version(secret_name) if secret_value is None: diff --git a/tests/test_cli.py b/tests/test_cli.py index fccceed..7ffe9df 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1477,3 +1477,75 @@ def test_output_multiline_secret_dotenv(tmp_path): expected_output = f'MY_MULTILINE_SECRET="{escaped_multiline_value}"' assert expected_output in result.stdout stubber.assert_no_pending_responses() + + +@patch("envars.main.GCPSecretManager") +def test_plaintext_gcp_file_output_without_gcp_credentials(mock_gcp_secret_manager, tmp_path): + mock_gcp_secret_manager.side_effect = Exception("no GCP credentials available") + + initial_content = """ +configuration: + kms_key: "projects/my-gcp-project/locations/us-central1/keyRings/my-key-ring/cryptoKeys/my-key" + environments: + - dev + locations: + - aws: "123456789012" + - gcp: "my-gcp-project" +environment_variables: + MY_VAR: + default: "plain-value" +""" + file_path = create_envars_file(tmp_path, initial_content) + result = runner.invoke(app, ["--file", file_path, "output", "--format", "yaml", "--env", "dev", "--loc", "aws"]) + assert result.exit_code == 0 + output_dict = yaml.safe_load(result.stdout) + assert output_dict["envars"]["MY_VAR"] == "plain-value" + mock_gcp_secret_manager.assert_not_called() + + +@patch("envars.main.CloudFormationExports") +@patch("envars.main.SSMParameterStore") +def test_plaintext_aws_file_output_without_aws_credentials(mock_ssm_store, mock_cf_exports, tmp_path): + mock_ssm_store.side_effect = Exception("no AWS credentials available") + mock_cf_exports.side_effect = Exception("no AWS credentials available") + + initial_content = """ +configuration: + kms_key: "arn:aws:kms:us-east-1:123456789012:key/mrk-12345" + environments: + - dev +environment_variables: + MY_VAR: + default: "plain-value" +""" + file_path = create_envars_file(tmp_path, initial_content) + result = runner.invoke(app, ["--file", file_path, "output", "--format", "yaml", "--env", "dev"]) + assert result.exit_code == 0 + output_dict = yaml.safe_load(result.stdout) + assert output_dict["envars"]["MY_VAR"] == "plain-value" + mock_ssm_store.assert_not_called() + mock_cf_exports.assert_not_called() + + +@patch("envars.main.CloudFormationExports") +@patch("envars.main.SSMParameterStore") +def test_parameter_store_lookup_does_not_construct_cf_exports_client(mock_ssm_store, mock_cf_exports, tmp_path): + mock_ssm_instance = mock_ssm_store.return_value + mock_ssm_instance.get_parameter.return_value = "ssm_value" + mock_cf_exports.side_effect = Exception("CloudFormationExports should not be constructed") + + initial_content = """ +configuration: + kms_key: "arn:aws:kms:us-east-1:123456789012:key/mrk-12345" + environments: + - dev +environment_variables: + MY_VAR: + default: "parameter_store:/my/parameter" +""" + file_path = create_envars_file(tmp_path, initial_content) + result = runner.invoke(app, ["--file", file_path, "output", "--format", "yaml", "--env", "dev"]) + assert result.exit_code == 0 + output_dict = yaml.safe_load(result.stdout) + assert output_dict["envars"]["MY_VAR"] == "ssm_value" + mock_cf_exports.assert_not_called()