class TestGetPlatformConfigPathFromArgs(unittest.TestCase): @given(filepath(), filepath()) def test_get_config_path_from_args(self, path_a, path_b): with patch('cdflow.abspath') as abspath: prefix = '/a/path' abspath.side_effect = lambda path: '{}/{}'.format(prefix, path) args = [ 'release', '--platform-config', path_a, '--platform-config={}'.format(path_b), '42' ] assert get_platform_config_paths(args) == [ '{}/{}'.format(prefix, path_a), '{}/{}'.format(prefix, path_b) ] def test_raises_exception_when_missing_flag(self): self.assertRaises(MissingPlatformConfigError, get_platform_config_paths, []) def test_raises_exception_when_missing_value(self): self.assertRaises( MissingPlatformConfigError, get_platform_config_paths, ['--platform-config'], )
class TestIntegration(unittest.TestCase): @given(filepath()) def test_release(self, project_root): argv = [ 'release', '--platform-config', '../path/to/config', '--release-data ami_id=ami-z9876', '42' ] with patch('cdflow.docker') as docker, \ patch('cdflow.os') as os, \ patch('cdflow.abspath') as abspath, \ patch('cdflow.open') as open_: abs_path_to_config = '/root/path/to/config' abspath.return_value = abs_path_to_config image = MagicMock(spec=Image) docker.from_env.return_value.images.pull.return_value = image image.attrs = {'RepoDigests': ['hash']} docker.from_env.return_value.containers.run.return_value.attrs = { 'State': { 'ExitCode': 0, } } os.getcwd.return_value = project_root os.getenv.return_value = False config_file = MagicMock(spec=TextIOWrapper) config_file.read.return_value = yaml.dump({'team': 'a-team'}) open_.return_value.__enter__.return_value = config_file exit_status = main(argv) assert exit_status == 0 docker.from_env.assert_called_once() docker.from_env.return_value.images.pull.assert_called_once_with( 'mergermarket/cdflow-commands:latest') docker.from_env.return_value.containers.run.assert_called_once_with( 'mergermarket/cdflow-commands:latest', command=argv, environment={ 'AWS_ACCESS_KEY_ID': ANY, 'AWS_SECRET_ACCESS_KEY': ANY, 'AWS_SESSION_TOKEN': ANY, 'FASTLY_API_KEY': ANY, 'GITHUB_TOKEN': ANY, 'CDFLOW_IMAGE_DIGEST': 'hash', 'LOGENTRIES_ACCOUNT_KEY': ANY, 'DATADOG_APP_KEY': ANY, 'DATADOG_API_KEY': ANY, }, detach=True, volumes={ project_root: { 'bind': project_root, 'mode': 'rw', }, abs_path_to_config: { 'bind': abs_path_to_config, 'mode': 'ro', }, '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro', }, }, working_dir=project_root) docker.from_env.return_value.containers.run.return_value.logs.\ assert_called_once_with( stream=True, follow=True, stdout=True, stderr=True, ) @given( fixed_dictionaries({ 'project_root': filepath(), 'environment': dictionaries( keys=text(alphabet=printable, min_size=1, max_size=3), values=text(alphabet=printable, min_size=1, max_size=3), max_size=3, ), 'image_id': image_id(), })) def test_release_with_pinned_command_image(self, fixtures): argv = ['release', '42', '--platform-config', 'path/to/config'] project_root = fixtures['project_root'] environment = fixtures['environment'] pinned_image_id = fixtures['image_id'] environment['CDFLOW_IMAGE_ID'] = pinned_image_id with patch('cdflow.docker') as docker, \ patch('cdflow.os') as os, \ patch('cdflow.abspath') as abspath, \ patch('cdflow.open') as open_: abs_path_to_config = '/root/path/to/config' abspath.return_value = abs_path_to_config image = MagicMock(spec=Image) docker.from_env.return_value.images.pull.return_value = image image.attrs = {'RepoDigests': ['hash']} docker.from_env.return_value.containers.run.return_value.attrs = { 'State': { 'ExitCode': 0, } } os.getcwd.return_value = project_root os.getenv.return_value = False os.environ = environment config_file = MagicMock(spec=TextIOWrapper) config_file.read.return_value = yaml.dump({'team': 'a-team'}) open_.return_value.__enter__.return_value = config_file exit_status = main(argv) assert exit_status == 0 docker.from_env.return_value.images.pull.assert_called_once_with( pinned_image_id) docker.from_env.return_value.containers.run.assert_called_once_with( pinned_image_id, command=argv, environment={ 'AWS_ACCESS_KEY_ID': ANY, 'AWS_SECRET_ACCESS_KEY': ANY, 'AWS_SESSION_TOKEN': ANY, 'FASTLY_API_KEY': ANY, 'GITHUB_TOKEN': ANY, 'CDFLOW_IMAGE_DIGEST': 'hash', 'LOGENTRIES_ACCOUNT_KEY': ANY, 'DATADOG_APP_KEY': ANY, 'DATADOG_API_KEY': ANY, }, detach=True, volumes={ project_root: { 'bind': project_root, 'mode': 'rw', }, abs_path_to_config: { 'bind': abs_path_to_config, 'mode': 'ro', }, '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro', }, }, working_dir=project_root) @given( fixed_dictionaries({ 'project_root': filepath(), 's3_bucket_and_key': s3_bucket_and_key(), 'release_bucket': text( alphabet=VALID_ALPHABET, min_size=3, max_size=5, ), })) def test_classic_deploy(self, fixtures): argv = ['deploy', 'aslive', '42'] with patch('cdflow.Session') as Session, \ patch('cdflow.BytesIO') as BytesIO, \ patch('cdflow.docker') as docker, \ patch('cdflow.os') as os, \ patch('cdflow.open') as open_: s3_resource = Mock() image_digest = 'sha:12345asdfg' s3_resource.Object.return_value.metadata = { 'cdflow_image_digest': image_digest } Session.return_value.resource.return_value = s3_resource BytesIO.return_value.__enter__.return_value.read.return_value = ''' {{ "release-bucket": "{}", "classic-metadata-handling": true }} '''.format(fixtures['release_bucket']) config_file = MagicMock(spec=TextIOWrapper) config_file.read.return_value = yaml.dump({ 'account-scheme-url': 's3://{}/{}'.format(*fixtures['s3_bucket_and_key']), 'team': 'a-team', }) open_.return_value.__enter__.return_value = config_file docker_client = MagicMock(spec=DockerClient) docker.from_env.return_value = docker_client docker.from_env.return_value.containers.run.return_value.attrs = { 'State': { 'ExitCode': 0, } } project_root = fixtures['project_root'] os.getcwd.return_value = project_root exit_status = main(argv) assert exit_status == 0 s3_resource.Object.assert_any_call( fixtures['s3_bucket_and_key'][0], fixtures['s3_bucket_and_key'][1], ) docker_client.containers.run.assert_called_once_with( image_digest, command=argv, environment=ANY, detach=True, volumes={ project_root: ANY, '/var/run/docker.sock': ANY }, working_dir=project_root, ) docker.from_env.return_value.containers.run.return_value.logs.\ assert_called_once_with( stream=True, follow=True, stdout=True, stderr=True, ) @given( fixed_dictionaries({ 'project_root': filepath(), 's3_bucket_and_key': s3_bucket_and_key(), 'release_bucket': text( alphabet=VALID_ALPHABET, min_size=3, max_size=5, ), 'team_name': text( alphabet=VALID_ALPHABET, min_size=3, max_size=5, ), 'component_name': text( alphabet=VALID_ALPHABET, min_size=3, max_size=5, ), })) def test_deploy(self, fixtures): version = '42' component_name = fixtures['component_name'] argv = ['deploy', 'aslive', version, '--component', component_name] with patch('cdflow.Session') as Session, \ patch('cdflow.BytesIO') as BytesIO, \ patch('cdflow.docker') as docker, \ patch('cdflow.os') as os, \ patch('cdflow.open') as open_: s3_resource = Mock() image_digest = 'sha:12345asdfg' s3_resource.Object.return_value.metadata = { 'cdflow_image_digest': image_digest } Session.return_value.resource.return_value = s3_resource BytesIO.return_value.__enter__.return_value.read.return_value = ''' {{ "release-bucket": "{}" }} '''.format(fixtures['release_bucket']) config_file = MagicMock(spec=TextIOWrapper) config_file.read.return_value = yaml.dump({ 'account-scheme-url': 's3://{}/{}'.format(*fixtures['s3_bucket_and_key']), 'team': fixtures['team_name'], }) open_.return_value.__enter__.return_value = config_file docker_client = MagicMock(spec=DockerClient) docker.from_env.return_value = docker_client docker.from_env.return_value.containers.run.return_value.attrs = { 'State': { 'ExitCode': 0, } } project_root = fixtures['project_root'] os.getcwd.return_value = project_root exit_status = main(argv) assert exit_status == 0 s3_resource.Object.assert_any_call( fixtures['s3_bucket_and_key'][0], fixtures['s3_bucket_and_key'][1], ) s3_resource.Object.assert_any_call( fixtures['release_bucket'], '{}/{}/{}-{}.zip'.format( fixtures['team_name'], component_name, component_name, version, ), ) docker_client.containers.run.assert_called_once_with( image_digest, command=argv, environment=ANY, detach=True, volumes={ project_root: ANY, '/var/run/docker.sock': ANY }, working_dir=project_root, ) docker.from_env.return_value.containers.run.return_value.logs.\ assert_called_once_with( stream=True, follow=True, stdout=True, stderr=True, ) @given(lists(elements=text(alphabet=printable, max_size=3), max_size=3)) def test_invalid_arguments_passed_to_container_to_handle(self, argv): with patch('cdflow.docker') as docker, \ patch('cdflow.os') as os, \ patch('cdflow.open') as open_: account_id = '1234567890' config_file = MagicMock(spec=TextIOWrapper) config_file.read.return_value = json.dumps( {'platform_config': { 'account_id': account_id }}) open_.return_value.__enter__.return_value = config_file error = ContainerError(container=CDFLOW_IMAGE_ID, exit_status=1, command=argv, image=CDFLOW_IMAGE_ID, stderr='help text') docker.from_env.return_value.containers.run.side_effect = error os.path.abspath.return_value = '/' exit_status = main(argv) assert exit_status == 1 docker.from_env.return_value.containers.run.assert_called_once_with( CDFLOW_IMAGE_ID, command=argv, environment=ANY, detach=True, volumes=ANY, working_dir=ANY)
class TestGetVersion(unittest.TestCase): @given( text(alphabet=VALID_ALPHABET, min_size=1).filter(lambda v: not v == '-v')) def test_get_version_during_deploy(self, version): argv = ['deploy', 'test', version] found_version = get_version(argv) assert found_version == version @given( text(alphabet=VALID_ALPHABET, min_size=1).filter(lambda v: not v == '-v')) def test_get_version_during_release(self, version): argv = ['release', version] found_version = get_version(argv) assert found_version == version @given( fixed_dictionaries({ 'version': (text(alphabet=VALID_ALPHABET, min_size=1).filter(lambda v: not v == '-v')), 'options': lists( elements=sampled_from( ('-c foo', '--component bar', '--platform-config path/to/config', '-v', '--verbose', '-p', '--plan-only')), unique=True, ), })) def test_get_version_when_options_present(self, fixtures): version = fixtures['version'] extra = fixtures['options'] + [version] shuffle(extra) extra = chain.from_iterable(e.split(' ') for e in extra) argv = ['release'] + list(extra) assert version == get_version(argv) def test_missing_version_returns_nothing(self): argv = ['release'] found_version = get_version(argv) assert found_version is None @given( fixed_dictionaries({ 'version': (text(alphabet=VALID_ALPHABET, min_size=1).filter(lambda v: not v == '-v')), 'path': filepath(), })) def test_get_version_when_platform_config_present(self, fixtures): version = fixtures['version'] path = fixtures['path'] argv = ['release', '--platform-config', path, version] found_version = get_version(argv) assert found_version == version
class TestDockerRun(unittest.TestCase): @given( fixed_dictionaries({ 'environment_variables': fixed_dictionaries({ 'AWS_ACCESS_KEY_ID': text(alphabet=printable, min_size=10), 'AWS_SECRET_ACCESS_KEY': text(alphabet=printable, min_size=10), 'AWS_SESSION_TOKEN': text(alphabet=printable, min_size=10), 'FASTLY_API_KEY': text(alphabet=printable, min_size=10), 'GITHUB_TOKEN': text(alphabet=printable, min_size=10), 'CDFLOW_IMAGE_DIGEST': text(min_size=12), }), 'image_id': image_id(), 'project_root': filepath(), 'platform_config_paths': lists(elements=filepath(), max_size=1), 'command': lists(text(alphabet=printable)), })) def test_run_args(self, fixtures): docker_client = MagicMock(spec=DockerClient) image_id = fixtures['image_id'] command = fixtures['command'] project_root = fixtures['project_root'] platform_config_paths = fixtures['platform_config_paths'] environment_variables = fixtures['environment_variables'] container = MagicMock(spec=Container) container.attrs = {'State': {'ExitCode': 0}} docker_client.containers.create.return_value = container exit_status, output = docker_run( docker_client, image_id, command, project_root, environment_variables, platform_config_paths, ) assert exit_status == 0 assert output == '' expected_volumes = { project_root: { 'bind': project_root, 'mode': 'rw', }, '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro', }, } for platform_config_path in platform_config_paths: expected_volumes[platform_config_path] = { 'bind': platform_config_path, 'mode': 'ro', } docker_client.containers.create.assert_called_once_with( image_id, command=command, environment=environment_variables, detach=True, volumes=expected_volumes, working_dir=project_root, ) container.start.assert_called_once() @given( fixed_dictionaries({ 'environment_variables': fixed_dictionaries({ 'AWS_ACCESS_KEY_ID': text(alphabet=printable, min_size=10), 'AWS_SECRET_ACCESS_KEY': text(alphabet=printable, min_size=10), 'AWS_SESSION_TOKEN': text(alphabet=printable, min_size=10), 'FASTLY_API_KEY': text(alphabet=printable, min_size=10), 'GITHUB_TOKEN': text(alphabet=printable, min_size=10), 'CDFLOW_IMAGE_DIGEST': text(min_size=12), }), 'image_id': image_id(), 'project_root': filepath(), 'command': lists(text(alphabet=printable)), })) def test_run_args_without_platform_config(self, fixtures): docker_client = MagicMock(spec=DockerClient) image_id = fixtures['image_id'] command = fixtures['command'] project_root = fixtures['project_root'] environment_variables = fixtures['environment_variables'] container = MagicMock(spec=Container) container.attrs = {'State': {'ExitCode': 0}} docker_client.containers.create.return_value = container exit_status, output = docker_run(docker_client, image_id, command, project_root, environment_variables) assert exit_status == 0 assert output == '' docker_client.containers.create.assert_called_once_with( image_id, command=command, environment=environment_variables, detach=True, volumes={ project_root: { 'bind': project_root, 'mode': 'rw', }, '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro', }, }, working_dir=project_root, ) container.start.assert_called_once() @given( fixed_dictionaries({ 'environment_variables': fixed_dictionaries({ 'AWS_ACCESS_KEY_ID': text(alphabet=printable, min_size=10), 'AWS_SECRET_ACCESS_KEY': text(alphabet=printable, min_size=10), 'AWS_SESSION_TOKEN': text(alphabet=printable, min_size=10), 'FASTLY_API_KEY': text(alphabet=printable, min_size=10), 'GITHUB_TOKEN': text(alphabet=printable, min_size=10), 'CDFLOW_IMAGE_DIGEST': text(min_size=12), }), 'image_id': image_id(), 'project_root': filepath(), 'command': lists(text(alphabet=printable)), })) def test_run_shell_command(self, fixtures): docker_client = MagicMock(spec=docker.from_env()) image_id = fixtures['image_id'] command = ['shell'] + fixtures['command'] project_root = fixtures['project_root'] environment_variables = fixtures['environment_variables'] container = MagicMock(spec=Container) container.attrs = {'State': {'ExitCode': 0}} docker_client.containers.create.return_value = container with patch('cdflow.dockerpty') as dockerpty, patch( 'cdflow.check_output', ) as check_output: check_output.return_value = '42\n' exit_status, output = docker_run(docker_client, image_id, command, project_root, environment_variables) dockerpty.start.assert_called_once_with( docker_client.api, container.id, ) assert exit_status == 0 assert output == 'Shell end' expected_environment_variable = deepcopy(environment_variables) expected_environment_variable['COLUMNS'] = 42 expected_environment_variable['LINES'] = 42 docker_client.containers.create.assert_called_once_with( image_id, command=command, environment=expected_environment_variable, volumes={ project_root: { 'bind': project_root, 'mode': 'rw', }, '/var/run/docker.sock': { 'bind': '/var/run/docker.sock', 'mode': 'ro', }, }, working_dir=project_root, tty=True, stdin_open=True, ) @given( fixed_dictionaries({ 'environment_variables': fixed_dictionaries({ 'AWS_ACCESS_KEY_ID': text(alphabet=printable, min_size=10), 'AWS_SECRET_ACCESS_KEY': text(alphabet=printable, min_size=10), 'AWS_SESSION_TOKEN': text(alphabet=printable, min_size=10), 'FASTLY_API_KEY': text(alphabet=printable, min_size=10), 'GITHUB_TOKEN': text(alphabet=printable, min_size=10), 'CDFLOW_IMAGE_DIGEST': text(min_size=12), }), 'image_id': image_id(), 'project_root': filepath(), 'platform_config_paths': lists(elements=filepath(), max_size=1), 'command': lists(text(alphabet=printable)), })) def test_error_from_docker(self, fixtures): image_id = fixtures['image_id'] command = fixtures['command'] project_root = fixtures['project_root'] platform_config_paths = fixtures['platform_config_paths'] environment_variables = fixtures['environment_variables'] docker_client = MagicMock(spec=DockerClient) docker_client.containers.create.side_effect = DockerException exit_status, output = docker_run( docker_client, image_id, command, project_root, environment_variables, platform_config_paths, ) assert exit_status == 1 assert output == str(DockerException()) @given( fixed_dictionaries({ 'image_id': image_id(), 'command': lists(text(alphabet=printable)), 'project_root': filepath(), 'platform_config_paths': lists(elements=filepath(), max_size=1), 'environment_variables': dictionaries( keys=text(alphabet=VALID_ALPHABET), values=text(alphabet=VALID_ALPHABET), min_size=1, ), })) def test_follow_container_logs(self, fixtures): docker_client = MagicMock(spec=DockerClient) container = MagicMock(spec=Container) logs = MagicMock() messages = [m.encode('utf-8') for m in ('Running', 'the', 'command')] logs.__iter__.return_value = iter(messages) container.logs.return_value = logs container.attrs = {'State': {'ExitCode': 0}} docker_client.containers.create.return_value = container with patch('cdflow.print') as print_: docker_run( docker_client, fixtures['image_id'], fixtures['command'], fixtures['project_root'], fixtures['environment_variables'], fixtures['platform_config_paths'], ) container.logs.assert_called_once_with(stream=True, follow=True, stdout=True, stderr=True) assert print_.call_args_list[0][1]['end'] == '' assert print_.call_args_list[0][0][0] == messages[0]\ .decode('utf-8') assert print_.call_args_list[1][0][0] == messages[1]\ .decode('utf-8') assert print_.call_args_list[2][0][0] == messages[2]\ .decode('utf-8') @given( fixed_dictionaries({ 'image_id': image_id(), 'command': lists(text(alphabet=printable)), 'project_root': filepath(), 'platform_config_paths': lists(elements=filepath(), max_size=1), 'environment_variables': dictionaries( keys=text(alphabet=VALID_ALPHABET), values=text(alphabet=VALID_ALPHABET), min_size=1, ), })) def test_container_can_be_removed_at_script_exit(self, fixtures): docker_client = MagicMock(spec=DockerClient) container = MagicMock(spec=Container) container.attrs = {'State': {'ExitCode': 0}} docker_client.containers.create.return_value = container with patch('cdflow.atexit') as atexit: docker_run( docker_client, fixtures['image_id'], fixtures['command'], fixtures['project_root'], fixtures['environment_variables'], fixtures['platform_config_paths'], ) atexit.register.assert_called_once_with(_remove_container, container) def test_remove_container(self): container = MagicMock(spec=Container) _remove_container(container) container.stop.assert_called_once_with() container.remove.assert_called_once_with() def test_remove_still_running_container(self): container = MagicMock(spec=Container) container.stop.side_effect = ReadTimeout try: _remove_container(container) except Exception as e: self.fail('Exception was raised: {}'.format(e)) container.stop.assert_called_once_with() container.remove.assert_called_once_with() @given( fixed_dictionaries({ 'image_id': image_id(), 'command': lists(text(alphabet=printable)), 'project_root': filepath(), 'platform_config_paths': lists(elements=filepath(), max_size=1), 'environment_variables': dictionaries( keys=text(alphabet=VALID_ALPHABET), values=text(alphabet=VALID_ALPHABET), min_size=1, ), 'exit_code': integers(min_value=-255, max_value=256), })) def test_exit_code_from_container_is_returned(self, fixtures): assume(fixtures['exit_code'] != 0) docker_client = MagicMock(spec=DockerClient) container = MagicMock(spec=Container) container.attrs = { 'State': { 'ExitCode': fixtures['exit_code'], } } docker_client.containers.create.return_value = container exit_status, output = docker_run( docker_client, fixtures['image_id'], fixtures['command'], fixtures['project_root'], fixtures['environment_variables'], fixtures['platform_config_paths'], ) assert exit_status == fixtures['exit_code'] assert output == ''