diff --git a/doc/changes/changes_1.1.0.md b/doc/changes/changes_1.1.0.md index 5ffe2c09..0557f20b 100644 --- a/doc/changes/changes_1.1.0.md +++ b/doc/changes/changes_1.1.0.md @@ -35,3 +35,4 @@ n/a * #220: Changed default ports in the external database configuration. * #221: Changed wording in the main configuration notebook, as suggested by PM. * #66: Used a non-root user to run Jupyter in the Docker Image ai-lab +* #149: Split AWS tests diff --git a/test/aws/fixtures.py b/test/aws/fixtures.py new file mode 100644 index 00000000..aba85608 --- /dev/null +++ b/test/aws/fixtures.py @@ -0,0 +1,78 @@ +import pytest + +from exasol.ds.sandbox.lib.tags import DEFAULT_TAG_KEY +from exasol.ds.sandbox.lib.asset_id import AssetId +from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket import create_vm_bucket_cf_template +from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket_waf import get_cloudformation_template +from exasol.ds.sandbox.lib.render_template import render_template +from test.aws.local_stack_access import AwsLocalStackAccess + +TEST_DUMMY_AMI_ID = "ami-123" +DEFAULT_ASSET_ID = AssetId("test", stack_prefix="test-stack", ami_prefix="test-ami") +TEST_ACL_ARN = "TEST-DOWNLOAD-ACL" +TEST_IP = "1.1.1.1" + + +@pytest.fixture +def default_asset_id(): + return DEFAULT_ASSET_ID + + +@pytest.fixture() +def test_dummy_ami_id(): + return TEST_DUMMY_AMI_ID + + +def vm_bucket_template(): + return create_vm_bucket_cf_template(TEST_ACL_ARN) + + +@pytest.fixture +def vm_bucket_cloudformation_yml(): + return vm_bucket_template() + + +def ec2_template(): + return render_template( + "ec2_cloudformation.jinja.yaml", + key_name="test_key", + user_name="test_user", + trace_tag=DEFAULT_TAG_KEY, + trace_tag_value=DEFAULT_ASSET_ID.tag_value, + ami_id=TEST_DUMMY_AMI_ID, + ) + + +@pytest.fixture +def ec2_cloudformation_yml(): + return ec2_template() + + +def waf_template(): + return get_cloudformation_template(TEST_IP) + + +@pytest.fixture +def waf_cloudformation_yml(): + return waf_template() + + +def ci_codebuild_template(): + return render_template( + "ci_code_build.jinja.yaml", + vm_bucket="test-bucket-123", + ) + + +def release_codebuild_template(): + return render_template( + "release_code_build.jinja.yaml", + vm_bucket="test-bucket-123", + path_in_bucket=AssetId.BUCKET_PREFIX, + dockerhub_secret_arn="secret_arn", + ) + + +@pytest.fixture(scope="session") +def local_stack_aws_access(local_stack): + return AwsLocalStackAccess().with_user("default_user") diff --git a/test/aws_local_stack_access.py b/test/aws/local_stack_access.py similarity index 100% rename from test/aws_local_stack_access.py rename to test/aws/local_stack_access.py diff --git a/test/aws/localstack.py b/test/aws/localstack.py new file mode 100644 index 00000000..a8a319d9 --- /dev/null +++ b/test/aws/localstack.py @@ -0,0 +1,39 @@ +import os +import pytest +import subprocess +import shlex + +from importlib.metadata import version + +@pytest.fixture(scope="session") +def local_stack(): + """ + This fixture starts/stops localstack as a context manager. + """ + command = "localstack start -d" + + image_version = version('localstack') + # See https://github.com/localstack/localstack/issues/8254 + # and https://github.com/localstack/localstack/issues/9939 + # + # Until an official release of localstack Docker image with a concrete + # version is available incl. a fix for issue 9939 we only can use version + # "latest". + # + # See https://github.com/exasol/ai-lab/issues/200 for replacing this with + # a concrete version in order to make CI tests more robust. + image_version = "3.2.0" + image_name = {"IMAGE_NAME": f"localstack/localstack:{image_version}"} + env_variables = {**os.environ, **image_name} + + process = subprocess.run(shlex.split(command), env=env_variables) + assert process.returncode == 0 + + command = "localstack wait -t 30" + + process = subprocess.run(shlex.split(command), env=env_variables) + assert process.returncode == 0 + yield None + + command = "localstack stop" + subprocess.run(shlex.split(command), env=env_variables) diff --git a/test/aws_mock_data.py b/test/aws/mock_data.py similarity index 98% rename from test/aws_mock_data.py rename to test/aws/mock_data.py index 3c6f2aed..b765b159 100644 --- a/test/aws_mock_data.py +++ b/test/aws/mock_data.py @@ -11,14 +11,15 @@ from exasol.ds.sandbox.lib.aws_access.snapshot import Snapshot from exasol.ds.sandbox.lib.aws_access.stack_resource import StackResource from exasol.ds.sandbox.lib.tags import create_default_asset_tag -from test.conftest import DEFAULT_ASSET_ID +from test.aws.fixtures import DEFAULT_ASSET_ID +from test.aws.fixtures import TEST_ACL_ARN TEST_ROLE_ID = 'VM-DSS-Bucket-VMImportRole-TEST' TEST_BUCKET_ID = 'vm-dss-bucket-vmdssbucket-TEST' TEST_AMI_ID = "AMI-IMAGE-12345" TEST_CLOUDFRONT_ID = "test-cloudfrontet-TEST" TEST_CLOUDFRONT_DOMAIN_NAME = "test-s3.cloudfront.net" -TEST_ACL_ARN = "TEST-DOWNLOAD-ACL" +# TEST_ACL_ARN = "TEST-DOWNLOAD-ACL" INSTANCE_ID = "test-instance" diff --git a/test/conftest.py b/test/conftest.py index fc9a7366..e7bb39c2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -10,20 +10,20 @@ from importlib.metadata import version from exasol.ds.sandbox.lib.tags import DEFAULT_TAG_KEY from exasol.ds.sandbox.lib.asset_id import AssetId -from test.aws_local_stack_access import AwsLocalStackAccess +from test.aws.local_stack_access import AwsLocalStackAccess -DEFAULT_ASSET_ID = AssetId("test", stack_prefix="test-stack", ami_prefix="test-ami") - -TEST_DUMMY_AMI_ID = "ami-123" +# DEFAULT_ASSET_ID = AssetId("test", stack_prefix="test-stack", ami_prefix="test-ami") +# +# TEST_DUMMY_AMI_ID = "ami-123" pytest_plugins = ( "test.docker.dss_docker_image", ) -@pytest.fixture -def default_asset_id(): - return DEFAULT_ASSET_ID +# @pytest.fixture +# def default_asset_id(): +# return DEFAULT_ASSET_ID @pytest.fixture @@ -31,50 +31,16 @@ def jupyter_port(): return 49494 -@pytest.fixture -def ec2_cloudformation_yml(): - return render_template("ec2_cloudformation.jinja.yaml", key_name="test_key", user_name="test_user", - trace_tag=DEFAULT_TAG_KEY, trace_tag_value=DEFAULT_ASSET_ID.tag_value, - ami_id=TEST_DUMMY_AMI_ID) - - -@pytest.fixture(scope="session") -def local_stack(): - """ - This fixture starts/stops localstack as a context manager. - """ - command = "localstack start -d" - - image_version = version('localstack') - # See https://github.com/localstack/localstack/issues/8254 - # and https://github.com/localstack/localstack/issues/9939 - # - # Until an official release of localstack Docker image with a concrete - # version is available incl. a fix for issue 9939 we only can use version - # "latest". - # - # See ai-lab issue for replacing this with a concrete version in order to - # make CI tests more robust. - image_version = "latest" - image_name = {"IMAGE_NAME": f"localstack/localstack:{image_version}"} - env_variables = {**os.environ, **image_name} - - process = subprocess.run(shlex.split(command), env=env_variables) - assert process.returncode == 0 - - command = "localstack wait -t 30" - - process = subprocess.run(shlex.split(command), env=env_variables) - assert process.returncode == 0 - yield None - - command = "localstack stop" - subprocess.run(shlex.split(command), env=env_variables) - - -@pytest.fixture(scope="session") -def local_stack_aws_access(local_stack): - return AwsLocalStackAccess().with_user("default_user") +# @pytest.fixture +# def ec2_cloudformation_yml(): +# return render_template( +# "ec2_cloudformation.jinja.yaml", +# key_name="test_key", +# user_name="test_user", +# trace_tag=DEFAULT_TAG_KEY, +# trace_tag_value=DEFAULT_ASSET_ID.tag_value, +# ami_id=TEST_DUMMY_AMI_ID, +# ) @pytest.fixture(scope="session") @@ -82,8 +48,3 @@ def test_config(): test_config = copy(default_config_object) test_config.time_to_wait_for_polling = 0.1 return test_config - - -@pytest.fixture() -def test_dummy_ami_id(): - return TEST_DUMMY_AMI_ID diff --git a/test/integration/aws/__init__.py b/test/integration/aws/__init__.py deleted file mode 100644 index a1d3903c..00000000 --- a/test/integration/aws/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -This package contains tests involving AWS resources. In order to execute -these tests you need an AWS account, a user with permissions in this account and -an access key. -""" diff --git a/test/integration/aws/cloudformation_validation.py b/test/integration/aws/cloudformation_validation.py deleted file mode 100644 index 7a30153a..00000000 --- a/test/integration/aws/cloudformation_validation.py +++ /dev/null @@ -1,19 +0,0 @@ -import subprocess - - -def validate_using_cfn_lint(tmp_path, cloudformation_yml): - """ - This test uses cfn-lint to validate the Cloudformation template. - (See https://github.com/aws-cloudformation/cfn-lint) - """ - out_file = tmp_path / "cloudformation.yaml" - with open(out_file, "w") as f: - f.write(cloudformation_yml) - - completed_process = subprocess.run(["cfn-lint", str(out_file.absolute())], capture_output=True) - try: - completed_process.check_returncode() - except subprocess.CalledProcessError as e: - print(e.stdout) - raise e - diff --git a/test/integration/localstack_test.py b/test/integration/aws/localstack_test.py similarity index 87% rename from test/integration/localstack_test.py rename to test/integration/aws/localstack_test.py index b3cd9ae3..dee8feee 100644 --- a/test/integration/localstack_test.py +++ b/test/integration/aws/localstack_test.py @@ -1,13 +1,27 @@ import botocore import pytest -from exasol.ds.sandbox.lib.setup_ec2.cf_stack import CloudformationStack, \ - CloudformationStackContextManager +from exasol.ds.sandbox.lib.setup_ec2.cf_stack import ( + CloudformationStack, + CloudformationStackContextManager, +) from exasol.ds.sandbox.lib.setup_ec2.run_setup_ec2 import run_lifecycle_for_ec2 from exasol.ds.sandbox.lib.tags import create_default_asset_tag - - -def test_ec2_lifecycle_with_local_stack(local_stack_aws_access, default_asset_id, test_dummy_ami_id): +from test.aws.fixtures import ( + default_asset_id, + test_dummy_ami_id, + ec2_cloudformation_yml, + local_stack_aws_access, +) +from test.aws.localstack import ( + local_stack, +) + +def test_ec2_lifecycle_with_local_stack( + local_stack_aws_access, + default_asset_id, + test_dummy_ami_id, +): """ This test uses localstack to simulate lifecycle of an EC-2 instance """ @@ -36,7 +50,11 @@ def test_ec2_manage_keypair_with_local_stack(local_stack_aws_access, default_ass aws.delete_ec2_key_pair("test") -def test_cloudformation_with_localstack(default_asset_id, local_stack_aws_access, ec2_cloudformation_yml): +def test_cloudformation_with_localstack( + default_asset_id, + local_stack_aws_access, + ec2_cloudformation_yml, +): aws = local_stack_aws_access aws.upload_cloudformation_stack( ec2_cloudformation_yml, stack_name="test_stack", diff --git a/test/integration/test_aws_codebuild_waiter.py b/test/integration/aws/test_codebuild_waiter.py similarity index 100% rename from test/integration/test_aws_codebuild_waiter.py rename to test/integration/aws/test_codebuild_waiter.py diff --git a/test/integration/aws/test_deploy_codebuild.py b/test/integration/aws/test_deploy_codebuild.py deleted file mode 100644 index e9234d3a..00000000 --- a/test/integration/aws/test_deploy_codebuild.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest - -from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess -from exasol.ds.sandbox.lib.render_template import render_template -from test.integration.aws.cloudformation_validation import validate_using_cfn_lint -from exasol.ds.sandbox.lib.asset_id import AssetId - - -codebuild_cloudformation_templates = [ - render_template( - "ci_code_build.jinja.yaml", - vm_bucket="test-bucket-123"), - render_template( - "release_code_build.jinja.yaml", - vm_bucket="test-bucket-123", - path_in_bucket=AssetId.BUCKET_PREFIX, - dockerhub_secret_arn="secret_arn") -] - - -@pytest.mark.parametrize("cloudformation_template", codebuild_cloudformation_templates) -def test_deploy_ci_codebuild_template(cloudformation_template): - aws_access = AwsAccess(None) - aws_access.validate_cloudformation_template(cloudformation_template) - - -@pytest.mark.parametrize("cloudformation_template", codebuild_cloudformation_templates) -def test_deploy_ci_codebuild_template_with_cnf_lint(tmp_path, cloudformation_template): - validate_using_cfn_lint(tmp_path, cloudformation_template) diff --git a/test/integration/aws/test_deploy_ec2.py b/test/integration/aws/test_deploy_ec2.py deleted file mode 100644 index 85b87da5..00000000 --- a/test/integration/aws/test_deploy_ec2.py +++ /dev/null @@ -1,45 +0,0 @@ -from unittest.mock import MagicMock - -from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess -from exasol.ds.sandbox.lib.setup_ec2.cf_stack import CloudformationStack, \ - CloudformationStackContextManager -from exasol.ds.sandbox.lib.tags import create_default_asset_tag -from test.integration.aws.cloudformation_validation import validate_using_cfn_lint - - -def test_deploy_ec2_upload_invoked(ec2_cloudformation_yml, default_asset_id, test_dummy_ami_id): - """" - Test if function upload_cloudformation_stack() will be invoked - with expected values when we run run_deploy_ci_build() - """ - aws_access_mock = MagicMock() - with CloudformationStackContextManager(CloudformationStack(aws_access_mock, "test_key", "test_user", - default_asset_id, - test_dummy_ami_id)) \ - as cf_access: - pass - default_tag = tuple(create_default_asset_tag(default_asset_id.tag_value)) - aws_access_mock.upload_cloudformation_stack.assert_called_once_with(ec2_cloudformation_yml, - cf_access.stack_name, - tags=default_tag) - - -def test_deploy_ec2_custom_prefix(ec2_cloudformation_yml, default_asset_id, test_dummy_ami_id): - """" - Test that the custom prefix will be used for the cloudformation stack name. - """ - aws_access_mock = MagicMock() - aws_access_mock.stack_exists.return_value = False - with CloudformationStackContextManager(CloudformationStack(aws_access_mock, - "test_key", "test_user", default_asset_id, - test_dummy_ami_id)) as cf_access: - assert cf_access.stack_name.startswith("test-stack") - - -def test_deploy_ec2_template(ec2_cloudformation_yml): - aws_access = AwsAccess(None) - aws_access.validate_cloudformation_template(ec2_cloudformation_yml) - - -def test_deploy_cec2_template_with_cnf_lint(tmp_path, ec2_cloudformation_yml): - validate_using_cfn_lint(tmp_path, ec2_cloudformation_yml) diff --git a/test/integration/aws/test_deploy_vm_bucket.py b/test/integration/aws/test_deploy_vm_bucket.py deleted file mode 100644 index 13746d53..00000000 --- a/test/integration/aws/test_deploy_vm_bucket.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import Union -from unittest.mock import Mock, create_autospec - -import pytest - -from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess -from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket import run_setup_vm_bucket, find_vm_bucket, \ - create_vm_bucket_cf_template -from test.aws_mock_data import TEST_BUCKET_ID, get_waf_cloudformation_mock_data, TEST_ACL_ARN, \ - get_s3_cloudformation_mock_data -from test.integration.aws.cloudformation_validation import validate_using_cfn_lint -from test.mock_cast import mock_cast -from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket import STACK_NAME as VM_STACK_NAME - - -@pytest.fixture -def vm_bucket_cloudformation_yml(): - return create_vm_bucket_cf_template(TEST_ACL_ARN) - - -def test_deploy_vm_bucket_template(vm_bucket_cloudformation_yml): - aws_access = AwsAccess(None) - aws_access.validate_cloudformation_template(vm_bucket_cloudformation_yml) - - -def test_deploy_vm_bucket_template_with_cnf_lint(tmp_path, vm_bucket_cloudformation_yml): - validate_using_cfn_lint(tmp_path, vm_bucket_cloudformation_yml) - - -def test_find_bucket_with_mock(test_config): - """ - This test uses a mock to validate the correct finding of the bucket in the stack. - """ - aws_access_mock: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) - mock_cast(aws_access_mock.describe_stacks).return_value = get_s3_cloudformation_mock_data() + \ - get_waf_cloudformation_mock_data() - mock_cast(aws_access_mock.instantiate_for_region).return_value = aws_access_mock - run_setup_vm_bucket(aws_access_mock, test_config) - mock_cast(aws_access_mock.upload_cloudformation_stack).assert_called_once() - - bucket = find_vm_bucket(aws_access_mock) - assert TEST_BUCKET_ID == bucket - - -def test_find_fails_if_vm_stack_not_deployed_with_mock(test_config): - """ - This test uses a mock to validate the raising of a RuntimeError exception if the VM bucket was not deployed. - """ - aws_access_mock: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) - mock_cast(aws_access_mock.describe_stacks).return_value = get_waf_cloudformation_mock_data() - mock_cast(aws_access_mock.instantiate_for_region).return_value = aws_access_mock - - with pytest.raises(RuntimeError, match=f"stack {VM_STACK_NAME} not found"): - find_vm_bucket(aws_access_mock) - - -def test_find_fails_if_waf_stack_not_deployed_with_mock(test_config): - """ - This test uses a mock to validate the raising of a RuntimeError exception if the WAF and VM bucket were not deployed. - """ - aws_access_mock: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) - - mock_cast(aws_access_mock.describe_stacks).return_value = list() - - with pytest.raises(RuntimeError, match=f"stack {VM_STACK_NAME} not found"): - find_vm_bucket(aws_access_mock) diff --git a/test/integration/aws/test_deploy_vm_bucket_waf.py b/test/integration/aws/test_deploy_vm_bucket_waf.py deleted file mode 100644 index 1f60d3e6..00000000 --- a/test/integration/aws/test_deploy_vm_bucket_waf.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Union -from unittest.mock import Mock, create_autospec - -import pytest - -from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess -from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket_waf import run_setup_vm_bucket_waf, \ - find_acl_arn, get_cloudformation_template -from test.aws_mock_data import get_waf_cloudformation_mock_data, TEST_ACL_ARN -from test.integration.aws.cloudformation_validation import validate_using_cfn_lint -from test.mock_cast import mock_cast - -TEST_IP = "1.1.1.1" - - -@pytest.fixture -def waf_cloudformation_yml(): - return get_cloudformation_template(TEST_IP) - - -def test_deploy_waf_template(waf_cloudformation_yml): - aws_access = AwsAccess(None) - aws_access.validate_cloudformation_template(waf_cloudformation_yml) - - -def test_deploy_waf_template_with_cnf_lint(tmp_path, waf_cloudformation_yml): - validate_using_cfn_lint(tmp_path, waf_cloudformation_yml) - - -def test_find_acl_arn(test_config): - """ - This test uses a mock to validate the correct finding of the ACL Arn in the mocked cloudformation stack. - """ - aws_access_mock: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) - mock_cast(aws_access_mock.describe_stacks).return_value = get_waf_cloudformation_mock_data() - mock_cast(aws_access_mock.instantiate_for_region).return_value = aws_access_mock - run_setup_vm_bucket_waf(aws_access_mock, allowed_ip=TEST_IP, config=test_config) - mock_cast(aws_access_mock.upload_cloudformation_stack).assert_called_once() - acl_arn = find_acl_arn(aws_access_mock, test_config) - assert TEST_ACL_ARN == acl_arn diff --git a/test/integration/aws/test_export_vm.py b/test/integration/aws/test_export_vm.py deleted file mode 100644 index 88e24ce0..00000000 --- a/test/integration/aws/test_export_vm.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import annotations - -from unittest.mock import call, create_autospec, MagicMock - -import pytest - -from exasol.ds.sandbox.lib.aws_access.ami import Ami -from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess -from exasol.ds.sandbox.lib.export_vm.rename_s3_objects import build_image_source, \ - build_image_destination -from exasol.ds.sandbox.lib.export_vm.run_export_vm import export_vm -from exasol.ds.sandbox.lib.export_vm.vm_disk_image_format import VmDiskImageFormat -from test.aws_mock_data import get_ami_image_mock_data, TEST_AMI_ID, TEST_ROLE_ID, TEST_BUCKET_ID, INSTANCE_ID, \ - get_export_image_task_mock_data, get_s3_cloudformation_mock_data, get_waf_cloudformation_mock_data -from test.mock_cast import mock_cast - - -def call_counter(func): - """ - Decorator which passes automatically a counter to the decorated function. - :param func: The decorated function - """ - - def helper(*args, **kwargs): - helper.calls += 1 - return func(helper.calls, *args, **kwargs) - - helper.calls = 0 - return helper - - -@call_counter -def get_ami_side_effect(counter: int, ami_id: str) -> Ami: - """ - This mock function returns a mocked Ami fascade object. - On the first 2 invocations the state of the object will be "pending" - On the 3rd time the returned state will be "available". - :raises ValueError if the parameter ami_id does not match the expected value. - """ - if ami_id == TEST_AMI_ID: - if counter < 3: - state = "pending" - else: - state = "available" - return get_ami_image_mock_data(state) - else: - raise ValueError(f"Unexpect parameter: {ami_id}") - - -@pytest.fixture -def aws_vm_export_mock(): - """ - Assembles an AwsAccess mock object which: - :return: 1. Returns the mocked VM Bucket Cloudformation stack when calling get_all_stack_resources() - 2. Returns TEST_AMI_ID when calling create_image_from_ec2_instance() - 3. Returns the mocked ExportImageTask object when calling get_export_image_task() - 4. Returns the mocked Ami object when calling get_ami() (see method get_ami_side_effect() for details - """ - aws_access_mock: AwsAccess | MagicMock = create_autospec(AwsAccess, spec_set=True) - mock_cast(aws_access_mock.describe_stacks).return_value = get_s3_cloudformation_mock_data() + \ - get_waf_cloudformation_mock_data() - mock_cast(aws_access_mock.create_image_from_ec2_instance).return_value = TEST_AMI_ID - mock_cast(aws_access_mock.get_export_image_task).return_value = get_export_image_task_mock_data(False) - mock_cast(aws_access_mock.get_ami).side_effect = get_ami_side_effect - return aws_access_mock - - -vm_formats = [ - (VmDiskImageFormat.VMDK,), - (VmDiskImageFormat.VHD,), - (VmDiskImageFormat.VHD, VmDiskImageFormat.VMDK), - tuple(vm_format for vm_format in VmDiskImageFormat), - tuple() -] - - -@pytest.mark.parametrize("vm_formats_to_test", vm_formats) -def test_export_vm(aws_vm_export_mock, default_asset_id, vm_formats_to_test, test_config): - """" - Test if function export_vm() will be invoked - with expected values when we run_export_vm() - """ - export_vm(aws_access=aws_vm_export_mock, instance_id=INSTANCE_ID, - vm_image_formats=tuple(vm_image_format.value for vm_image_format in vm_formats_to_test), - asset_id=default_asset_id, configuration=test_config) - - mock_cast(aws_vm_export_mock.create_image_from_ec2_instance). \ - assert_called_once_with(INSTANCE_ID, - name=default_asset_id.ami_name, - tag_value=default_asset_id.tag_value, - description="Image Description") - - expected_calls = [ - call(image_id=TEST_AMI_ID, - tag_value=default_asset_id.tag_value, - description="VM Description", - role_name=TEST_ROLE_ID, - disk_format=disk_format, - s3_bucket=TEST_BUCKET_ID, - s3_prefix=f"{default_asset_id.bucket_prefix}/") - for disk_format in vm_formats_to_test] - assert mock_cast(aws_vm_export_mock.export_ami_image_to_vm).call_args_list == expected_calls - - expected_calls_copy = list() - expected_calls_delete = list() - for disk_format in vm_formats_to_test: - source = build_image_source(prefix=default_asset_id.bucket_prefix, - export_image_task_id="export-ami-123", - vm_image_format=disk_format) - dest = build_image_destination(prefix=default_asset_id.bucket_prefix, asset_id=default_asset_id, - vm_image_format=disk_format) - expected_calls_copy.append(call(bucket=TEST_BUCKET_ID, source=source, dest=dest)) - expected_calls_delete.append(call(bucket=TEST_BUCKET_ID, source=source)) - - assert mock_cast(aws_vm_export_mock.copy_large_s3_object).call_args_list == expected_calls_copy - assert mock_cast(aws_vm_export_mock.delete_s3_object).call_args_list == expected_calls_delete diff --git a/test/unit/aws/test_deploy_ec2.py b/test/unit/aws/test_deploy_ec2.py new file mode 100644 index 00000000..f56146ec --- /dev/null +++ b/test/unit/aws/test_deploy_ec2.py @@ -0,0 +1,48 @@ +from unittest.mock import MagicMock + +from exasol.ds.sandbox.lib.setup_ec2.cf_stack import ( + CloudformationStack, + CloudformationStackContextManager, +) +from exasol.ds.sandbox.lib.tags import create_default_asset_tag +from test.aws.fixtures import ( + default_asset_id, + ec2_cloudformation_yml, + test_dummy_ami_id, +) + + +def test_deploy_ec2_upload_invoked(ec2_cloudformation_yml, default_asset_id, test_dummy_ami_id): + """" + Test if function upload_cloudformation_stack() will be invoked with + expected values when we run run_deploy_ci_build() + """ + aws = MagicMock() + with CloudformationStackContextManager( + CloudformationStack( + aws, + "test_key", + "test_user", + default_asset_id, + test_dummy_ami_id, + )) as cloudformation: + pass + default_tag = tuple(create_default_asset_tag(default_asset_id.tag_value)) + aws.upload_cloudformation_stack.assert_called_once_with( + ec2_cloudformation_yml, + cloudformation.stack_name, + tags=default_tag, + ) + + +def test_deploy_ec2_custom_prefix(ec2_cloudformation_yml, default_asset_id, test_dummy_ami_id): + """" + Test that the custom prefix will be used for the cloudformation stack name. + """ + aws = MagicMock() + aws.stack_exists.return_value = False + with CloudformationStackContextManager( + CloudformationStack( + aws, "test_key", "test_user", default_asset_id, test_dummy_ami_id, + )) as cloudformation: + assert cloudformation.stack_name.startswith("test-stack") diff --git a/test/unit/aws/test_deploy_vm_bucket.py b/test/unit/aws/test_deploy_vm_bucket.py new file mode 100644 index 00000000..1f3d38d8 --- /dev/null +++ b/test/unit/aws/test_deploy_vm_bucket.py @@ -0,0 +1,59 @@ +from typing import Union +from unittest.mock import Mock, create_autospec + +import pytest + +from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess +from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket import ( + run_setup_vm_bucket, + find_vm_bucket, + create_vm_bucket_cf_template, +) +from test.aws.mock_data import ( + TEST_BUCKET_ID, + get_waf_cloudformation_mock_data, + get_s3_cloudformation_mock_data, +) +from test.mock_cast import mock_cast +from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket import STACK_NAME as VM_STACK_NAME + + +def test_find_bucket_success(test_config): + """ + This test uses a mock to validate the correct finding of the bucket in the stack. + """ + aws: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) + mock_cast(aws.describe_stacks).return_value = \ + get_s3_cloudformation_mock_data() + \ + get_waf_cloudformation_mock_data() + mock_cast(aws.instantiate_for_region).return_value = aws + run_setup_vm_bucket(aws, test_config) + mock_cast(aws.upload_cloudformation_stack).assert_called_once() + + bucket = find_vm_bucket(aws) + assert TEST_BUCKET_ID == bucket + + +def test_vm_bucket_undeployed(test_config): + """ + This test uses a mock to validate the raising of a RuntimeError + exception if the VM bucket was not deployed. + """ + aws: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) + mock_cast(aws.describe_stacks).return_value = get_waf_cloudformation_mock_data() + mock_cast(aws.instantiate_for_region).return_value = aws + + with pytest.raises(RuntimeError, match=f"stack {VM_STACK_NAME} not found"): + find_vm_bucket(aws) + + +def test_waf_undeployed(test_config): + """ + This test uses a mock to validate the raising of a RuntimeError + exception if the WAF and VM bucket were not deployed. + """ + aws: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) + mock_cast(aws.describe_stacks).return_value = list() + + with pytest.raises(RuntimeError, match=f"stack {VM_STACK_NAME} not found"): + find_vm_bucket(aws) diff --git a/test/unit/aws/test_deploy_vm_bucket_waf.py b/test/unit/aws/test_deploy_vm_bucket_waf.py new file mode 100644 index 00000000..bf17a74f --- /dev/null +++ b/test/unit/aws/test_deploy_vm_bucket_waf.py @@ -0,0 +1,26 @@ +from typing import Union +from unittest.mock import Mock, create_autospec + +from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess +from exasol.ds.sandbox.lib.vm_bucket.vm_dss_bucket_waf import ( + run_setup_vm_bucket_waf, + find_acl_arn, + get_cloudformation_template, +) +from test.aws.fixtures import TEST_IP +from test.aws.mock_data import get_waf_cloudformation_mock_data, TEST_ACL_ARN +from test.mock_cast import mock_cast + + +def test_find_acl_arn(test_config): + """ + This test uses a mock to validate the correct finding of the ACL Arn + in the mocked cloudformation stack. + """ + aws: Union[AwsAccess, Mock] = create_autospec(AwsAccess, spec_set=True) + mock_cast(aws.describe_stacks).return_value = get_waf_cloudformation_mock_data() + mock_cast(aws.instantiate_for_region).return_value = aws + run_setup_vm_bucket_waf(aws, allowed_ip=TEST_IP, config=test_config) + mock_cast(aws.upload_cloudformation_stack).assert_called_once() + acl_arn = find_acl_arn(aws, test_config) + assert TEST_ACL_ARN == acl_arn diff --git a/test/unit/aws/test_export_vm.py b/test/unit/aws/test_export_vm.py new file mode 100644 index 00000000..363582d6 --- /dev/null +++ b/test/unit/aws/test_export_vm.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from unittest.mock import call, create_autospec, MagicMock + +import pytest + +from exasol.ds.sandbox.lib.aws_access.ami import Ami +from exasol.ds.sandbox.lib.aws_access.aws_access import AwsAccess +from exasol.ds.sandbox.lib.export_vm.rename_s3_objects import ( + build_image_source, + build_image_destination, +) +from exasol.ds.sandbox.lib.export_vm.run_export_vm import export_vm +from exasol.ds.sandbox.lib.export_vm.vm_disk_image_format import VmDiskImageFormat +from test.aws.mock_data import ( + get_ami_image_mock_data, + TEST_AMI_ID, + TEST_ROLE_ID, + TEST_BUCKET_ID, + INSTANCE_ID, + get_export_image_task_mock_data, + get_s3_cloudformation_mock_data, + get_waf_cloudformation_mock_data, +) +from test.aws.fixtures import default_asset_id +from test.mock_cast import mock_cast + + +def call_counter(func): + """ + Decorator which passes automatically a counter to the decorated + function. :param func: The decorated function + """ + def helper(*args, **kwargs): + helper.calls += 1 + return func(helper.calls, *args, **kwargs) + + helper.calls = 0 + return helper + + +@call_counter +def get_ami_side_effect(counter: int, ami_id: str) -> Ami: + """ + This mock function returns a mocked Ami fascade object. + On the first 2 invocations the state of the object will be "pending" + On the 3rd time the returned state will be "available". + :raises ValueError if the parameter ami_id does not match the expected value. + """ + if ami_id == TEST_AMI_ID: + if counter < 3: + state = "pending" + else: + state = "available" + return get_ami_image_mock_data(state) + else: + raise ValueError(f"Unexpect parameter: {ami_id}") + + +@pytest.fixture +def aws_vm_export_mock(): + """ + Assembles an AwsAccess mock object which: + :return: + 1. the mocked VM Bucket Cloudformation stack when calling + get_all_stack_resources() + 2. TEST_AMI_ID when calling create_image_from_ec2_instance() + 3. the mocked ExportImageTask object when calling get_export_image_task() + 4. the mocked Ami object when calling get_ami() + (see method get_ami_side_effect() for details) + """ + aws: AwsAccess | MagicMock = create_autospec(AwsAccess, spec_set=True) + mock_cast(aws.describe_stacks).return_value = \ + get_s3_cloudformation_mock_data() + \ + get_waf_cloudformation_mock_data() + mock_cast(aws.create_image_from_ec2_instance).return_value = TEST_AMI_ID + mock_cast(aws.get_export_image_task).return_value = get_export_image_task_mock_data(False) + mock_cast(aws.get_ami).side_effect = get_ami_side_effect + return aws + + +vm_formats = [ + (VmDiskImageFormat.VMDK,), + (VmDiskImageFormat.VHD,), + (VmDiskImageFormat.VHD, VmDiskImageFormat.VMDK), + tuple(vm_format for vm_format in VmDiskImageFormat), + tuple() +] + + +@pytest.mark.parametrize("vm_formats_to_test", vm_formats) +def test_export_vm(aws_vm_export_mock, default_asset_id, vm_formats_to_test, test_config): + """" + Test if function export_vm() will be invoked + with expected values when we run_export_vm() + """ + aws = aws_vm_export_mock + export_vm( + aws_access=aws, + instance_id=INSTANCE_ID, + vm_image_formats=tuple(fmt.value for fmt in vm_formats_to_test), + asset_id=default_asset_id, + configuration=test_config, + ) + + mock_cast(aws.create_image_from_ec2_instance). \ + assert_called_once_with( + INSTANCE_ID, + name=default_asset_id.ami_name, + tag_value=default_asset_id.tag_value, + description="Image Description", + ) + + expected_calls = [ + call(image_id=TEST_AMI_ID, + tag_value=default_asset_id.tag_value, + description="VM Description", + role_name=TEST_ROLE_ID, + disk_format=disk_format, + s3_bucket=TEST_BUCKET_ID, + s3_prefix=f"{default_asset_id.bucket_prefix}/") + for disk_format in vm_formats_to_test] + assert mock_cast(aws.export_ami_image_to_vm).call_args_list == expected_calls + + expected_calls_copy = list() + expected_calls_delete = list() + for disk_format in vm_formats_to_test: + source = build_image_source( + prefix=default_asset_id.bucket_prefix, + export_image_task_id="export-ami-123", + vm_image_format=disk_format, + ) + dest = build_image_destination( + prefix=default_asset_id.bucket_prefix, + asset_id=default_asset_id, + vm_image_format=disk_format, + ) + expected_calls_copy.append(call(bucket=TEST_BUCKET_ID, source=source, dest=dest)) + expected_calls_delete.append(call(bucket=TEST_BUCKET_ID, source=source)) + + assert mock_cast(aws.copy_large_s3_object).call_args_list == expected_calls_copy + assert mock_cast(aws.delete_s3_object).call_args_list == expected_calls_delete diff --git a/test/unit/aws/test_lint_cloudformation_templates.py b/test/unit/aws/test_lint_cloudformation_templates.py new file mode 100644 index 00000000..fa3379c2 --- /dev/null +++ b/test/unit/aws/test_lint_cloudformation_templates.py @@ -0,0 +1,42 @@ +# The tests in this file validate the various cloudformation templates by +# using the cloudformation linter "cfn-lint". + +import pytest +import subprocess + +from test.aws.fixtures import vm_bucket_cloudformation_yml +from test.aws.fixtures import ( + ec2_template, + waf_template, + vm_bucket_template, + ci_codebuild_template, + release_codebuild_template, +) + + +def validate_using_cfn_lint(tmp_path, cloudformation_yml): + """ + This test uses cfn-lint to validate the Cloudformation template. + (See https://github.com/aws-cloudformation/cfn-lint) + """ + out_file = tmp_path / "cloudformation.yaml" + with open(out_file, "w") as f: + f.write(cloudformation_yml) + + completed_process = subprocess.run(["cfn-lint", str(out_file.absolute())], capture_output=True) + try: + completed_process.check_returncode() + except subprocess.CalledProcessError as e: + print(e.stdout) + raise e + + +@pytest.mark.parametrize("template", [ + ci_codebuild_template(), + release_codebuild_template(), + ec2_template(), + vm_bucket_template(), + waf_template(), +]) +def test_lint_cloudformation_templates(tmp_path, template): + validate_using_cfn_lint(tmp_path, template) diff --git a/test/unit/test_make_ami_public.py b/test/unit/aws/test_make_ami_public.py similarity index 100% rename from test/unit/test_make_ami_public.py rename to test/unit/aws/test_make_ami_public.py diff --git a/test/unit/test_source_ami.py b/test/unit/aws/test_source_ami.py similarity index 99% rename from test/unit/test_source_ami.py rename to test/unit/aws/test_source_ami.py index 06bad529..fed519ba 100644 --- a/test/unit/test_source_ami.py +++ b/test/unit/aws/test_source_ami.py @@ -997,9 +997,9 @@ def test_find_source_ami_returns_latest_ami(test_config): """ Test that find_source_ami returns the latest AMI image based on the filters given in the global config. """ - aws_mock = MagicMock() - aws_mock.list_amis.return_value = mock_data - latest_ami = find_source_ami(aws_mock, test_config.source_ami_filters) + aws = MagicMock() + aws.list_amis.return_value = mock_data + latest_ami = find_source_ami(aws, test_config.source_ami_filters) # ami-0d203747b007677da is the latest one in the mock data assert latest_ami.id == "ami-0d203747b007677da"