mirror of
https://github.com/encounter/sceptre.git
synced 2026-03-30 11:37:13 -07:00
500 lines
18 KiB
Python
500 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import pytest
|
|
from mock import patch, sentinel, Mock, PropertyMock
|
|
|
|
from botocore.exceptions import ClientError
|
|
|
|
from sceptre.exceptions import CircularDependenciesError
|
|
from sceptre.exceptions import StackDoesNotExistError
|
|
from sceptre.exceptions import InvalidEnvironmentPathError
|
|
|
|
from sceptre.environment import Environment
|
|
from sceptre.stack_status import StackStatus
|
|
|
|
|
|
class TestEnvironment(object):
|
|
|
|
@patch("sceptre.environment.Environment._load_stacks")
|
|
@patch(
|
|
"sceptre.environment.Environment.is_leaf", new_callable=PropertyMock
|
|
)
|
|
@patch("sceptre.environment.Environment._validate_path")
|
|
def setup_method(
|
|
self, test_method, mock_validate_path,
|
|
mock_is_leaf, mock_load_stacks
|
|
):
|
|
mock_is_leaf.return_value = True
|
|
mock_load_stacks.return_value = sentinel.stacks
|
|
mock_validate_path.return_value = "environment_path"
|
|
|
|
self.environment = Environment(
|
|
sceptre_dir="sceptre_dir",
|
|
environment_path="environment_path",
|
|
options=sentinel.options
|
|
)
|
|
|
|
# Run the rest of the tests against a leaf environment
|
|
self.environment._is_leaf = True
|
|
|
|
def test_initialise_environment(self):
|
|
assert self.environment.sceptre_dir == "sceptre_dir"
|
|
assert self.environment.path == "environment_path"
|
|
assert self.environment._options == sentinel.options
|
|
assert self.environment.is_leaf is True
|
|
assert self.environment.stacks == sentinel.stacks
|
|
|
|
@patch("sceptre.environment.Environment._load_environments")
|
|
@patch(
|
|
"sceptre.environment.Environment.is_leaf", new_callable=PropertyMock
|
|
)
|
|
@patch("sceptre.environment.Environment._validate_path")
|
|
def test_initialise_environment_with_non_leaf_directory(
|
|
self, mock_validate_path,
|
|
mock_is_leaf, mock_load_environments
|
|
):
|
|
mock_is_leaf.return_value = False
|
|
mock_load_environments.return_value = sentinel.environments
|
|
|
|
environment = Environment(
|
|
sceptre_dir="sceptre_dir",
|
|
environment_path="environment_path",
|
|
options=sentinel.options
|
|
)
|
|
|
|
assert environment.environments == sentinel.environments
|
|
|
|
def test_repr(self):
|
|
self.environment.path = "path"
|
|
self.environment.sceptre_dir = "sceptre_dir"
|
|
self.environment._options = {}
|
|
response = self.environment.__repr__()
|
|
assert response == (
|
|
"sceptre.environment.Environment(sceptre_dir='sceptre_dir', "
|
|
"environment_path='path', options={})"
|
|
)
|
|
|
|
def test_validate_path_with_valid_path(self):
|
|
path = self.environment._validate_path("valid/env/path")
|
|
assert path == "valid/env/path"
|
|
|
|
def test_validate_path_with_backslashes_in_path(self):
|
|
path = self.environment._validate_path("valid\env\path")
|
|
assert path == "valid/env/path"
|
|
|
|
def test_validate_path_with_double_backslashes_in_path(self):
|
|
path = self.environment._validate_path("valid\\env\\path")
|
|
assert path == "valid/env/path"
|
|
|
|
def test_validate_path_with_leading_slash(self):
|
|
with pytest.raises(InvalidEnvironmentPathError):
|
|
self.environment._validate_path(
|
|
"/this/environment/path/is/invalid"
|
|
)
|
|
|
|
def test_validate_path_with_leading_backslash(self):
|
|
with pytest.raises(InvalidEnvironmentPathError):
|
|
self.environment._validate_path(
|
|
"\\this\environment\path\is\invalid"
|
|
)
|
|
|
|
def test_validate_path_with_trailing_slash(self):
|
|
with pytest.raises(InvalidEnvironmentPathError):
|
|
self.environment._validate_path(
|
|
"this/environment/path/is/invalid/"
|
|
)
|
|
|
|
def test_validate_path_with_trailing_backslash(self):
|
|
with pytest.raises(InvalidEnvironmentPathError):
|
|
self.environment._validate_path(
|
|
"this\environment\path\is\invalid\\"
|
|
)
|
|
|
|
def test_is_leaf_with_leaf_dir(self):
|
|
self.environment.sceptre_dir = os.path.join(
|
|
os.getcwd(), "tests", "fixtures"
|
|
)
|
|
self.environment.path = os.path.join(
|
|
"account", "environment", "region"
|
|
)
|
|
self.environment._is_leaf = None
|
|
assert self.environment.is_leaf is True
|
|
|
|
def test_is_leaf_with_non_leaf_dir(self):
|
|
self.environment.sceptre_dir = os.path.join(
|
|
os.getcwd(), "tests", "fixtures"
|
|
)
|
|
self.environment.path = os.path.join(
|
|
"account", "environment"
|
|
)
|
|
self.environment._is_leaf = None
|
|
assert self.environment.is_leaf is False
|
|
|
|
@patch("sceptre.environment.Environment._build")
|
|
@patch("sceptre.environment.Environment._check_for_circular_dependencies")
|
|
@patch("sceptre.environment.Environment._get_launch_dependencies")
|
|
@patch("sceptre.environment.Environment._get_initial_statuses")
|
|
@patch("sceptre.environment.Environment._get_threading_events")
|
|
def test_launch_calls_build_with_correct_args(
|
|
self, mock_get_threading_events, mock_get_initial_statuses,
|
|
mock_get_launch_dependencies, mock_check_for_circular_dependencies,
|
|
mock_build
|
|
):
|
|
mock_get_threading_events.return_value = sentinel.threading_events
|
|
mock_get_initial_statuses.return_value = sentinel.stack_statuses
|
|
mock_get_launch_dependencies.return_value = \
|
|
sentinel.dependencies
|
|
|
|
self.environment.launch()
|
|
|
|
mock_check_for_circular_dependencies.assert_called_once_with(
|
|
sentinel.dependencies
|
|
)
|
|
mock_build.assert_called_once_with(
|
|
"launch", sentinel.threading_events,
|
|
sentinel.stack_statuses, sentinel.dependencies
|
|
)
|
|
|
|
@patch("sceptre.environment.Environment._build")
|
|
@patch("sceptre.environment.Environment._check_for_circular_dependencies")
|
|
@patch("sceptre.environment.Environment._get_delete_dependencies")
|
|
@patch("sceptre.environment.Environment._get_initial_statuses")
|
|
@patch("sceptre.environment.Environment._get_threading_events")
|
|
def test_delete_calls_build_with_correct_args(
|
|
self, mock_get_threading_events, mock_get_initial_statuses,
|
|
mock_get_delete_dependencies, mock_check_for_circular_dependencies,
|
|
mock_build
|
|
):
|
|
mock_get_threading_events.return_value = sentinel.threading_events
|
|
mock_get_initial_statuses.return_value = sentinel.stack_statuses
|
|
mock_get_delete_dependencies.return_value = \
|
|
sentinel.dependencies
|
|
|
|
self.environment.delete()
|
|
|
|
mock_check_for_circular_dependencies.assert_called_once_with(
|
|
sentinel.dependencies
|
|
)
|
|
mock_build.assert_called_once_with(
|
|
"delete", sentinel.threading_events,
|
|
sentinel.stack_statuses, sentinel.dependencies
|
|
)
|
|
|
|
def test_describe_with_running_stack(self):
|
|
mock_stack = Mock()
|
|
mock_stack.name = "stack"
|
|
mock_stack.get_status.return_value = "status"
|
|
self.environment.stacks = {"name": mock_stack}
|
|
|
|
response = self.environment.describe()
|
|
assert response == {"stack": "status"}
|
|
|
|
def test_describe_with_missing_stack(self):
|
|
mock_stack = Mock()
|
|
mock_stack.name = "stack"
|
|
mock_stack.get_status.side_effect = StackDoesNotExistError
|
|
self.environment.stacks = {"stack": mock_stack}
|
|
|
|
response = self.environment.describe()
|
|
assert response == {"stack": "PENDING"}
|
|
|
|
def test_describe_resources_forms_response(self):
|
|
mock_stack = Mock()
|
|
mock_stack.name = "stack-name"
|
|
mock_stack.describe_resources.return_value = [
|
|
{
|
|
"LogicalResourceId": sentinel.logical_resource_id,
|
|
"PhysicalResourceId": sentinel.physical_resource_id
|
|
}
|
|
]
|
|
|
|
self.environment.stacks = {"stack-name": mock_stack}
|
|
response = self.environment.describe_resources()
|
|
assert response == {
|
|
"stack-name": [
|
|
{
|
|
"LogicalResourceId": sentinel.logical_resource_id,
|
|
"PhysicalResourceId": sentinel.physical_resource_id
|
|
}
|
|
]
|
|
}
|
|
|
|
def test_describe_resources_ignores_stack_does_not_exist_exception(self):
|
|
mock_stack = Mock()
|
|
mock_stack.full_stack_name = sentinel.full_stack_name
|
|
mock_stack.describe_resources.side_effect = ClientError(
|
|
{
|
|
"Error": {
|
|
"Code": 500,
|
|
"Message": "stack does not exist"
|
|
}
|
|
},
|
|
sentinel.operation
|
|
)
|
|
|
|
self.environment.stacks = {"name": mock_stack}
|
|
response = self.environment.describe_resources()
|
|
assert response == {}
|
|
|
|
def test_describe_resources_raises_other_client_errors(self):
|
|
mock_stack = Mock()
|
|
mock_stack.full_stack_name = sentinel.full_stack_name
|
|
mock_stack.describe_resources.side_effect = ClientError(
|
|
{
|
|
"Error": {
|
|
"Code": 500,
|
|
"Message": "Boom!"
|
|
}
|
|
},
|
|
sentinel.operation
|
|
)
|
|
|
|
self.environment.stacks = {"mock_stack": mock_stack}
|
|
with pytest.raises(ClientError):
|
|
self.environment.describe_resources()
|
|
|
|
@patch("sceptre.environment.wait")
|
|
@patch("sceptre.environment.ThreadPoolExecutor")
|
|
def test_build(self, mock_ThreadPoolExecutor, mock_wait):
|
|
self.environment.stacks = {"mock_stack": sentinel.stack}
|
|
mock_ThreadPoolExecutor.return_value.__enter__.return_value\
|
|
.submit.return_value = sentinel.future
|
|
|
|
self.environment._build(
|
|
sentinel.command, sentinel.threading_events,
|
|
sentinel.stack_statuses, sentinel.dependencies
|
|
)
|
|
|
|
mock_wait.assert_called_once_with([sentinel.future])
|
|
|
|
def test_manage_stack_build_with_failed_dependency(self):
|
|
threading_events = {"stack_1": Mock(), "stack_2": Mock()}
|
|
stack_statuses = {
|
|
"stack_1": StackStatus.FAILED,
|
|
"stack_2": StackStatus.PENDING
|
|
}
|
|
dependencies = {"stack_1": [], "stack_2": ["stack_1"]}
|
|
|
|
mock_stack_2 = Mock()
|
|
mock_stack_2.name = "stack_2"
|
|
|
|
self.environment._manage_stack_build(
|
|
mock_stack_2,
|
|
sentinel.command,
|
|
threading_events,
|
|
stack_statuses,
|
|
dependencies
|
|
)
|
|
|
|
assert stack_statuses["stack_2"] == StackStatus.FAILED
|
|
# Check that that stack's event is set
|
|
threading_events["stack_2"].set.assert_called_once_with()
|
|
|
|
def test_manage_stack_build_with_sucessful_command(self):
|
|
threading_events = {"stack": Mock()}
|
|
stack_statuses = {"stack": StackStatus.PENDING}
|
|
dependencies = {"stack": []}
|
|
|
|
mock_stack = Mock()
|
|
mock_stack.name = "stack"
|
|
mock_stack.launch.return_value = StackStatus.COMPLETE
|
|
|
|
self.environment._manage_stack_build(
|
|
mock_stack,
|
|
"launch",
|
|
threading_events,
|
|
stack_statuses,
|
|
dependencies
|
|
)
|
|
|
|
assert stack_statuses["stack"] == StackStatus.COMPLETE
|
|
# Check that that stack's event is set
|
|
threading_events["stack"].set.assert_called_once_with()
|
|
|
|
def test_manage_stack_build_with_unsucessful_command(self):
|
|
threading_events = {"stack": Mock()}
|
|
stack_statuses = {"stack": StackStatus.PENDING}
|
|
dependencies = {"stack": []}
|
|
|
|
mock_stack = Mock()
|
|
mock_stack.name = "stack"
|
|
mock_stack.launch.side_effect = Exception()
|
|
|
|
self.environment._manage_stack_build(
|
|
mock_stack,
|
|
"launch",
|
|
threading_events,
|
|
stack_statuses,
|
|
dependencies
|
|
)
|
|
|
|
assert stack_statuses["stack"] == StackStatus.FAILED
|
|
# Check that that stack's event is set
|
|
threading_events["stack"].set.assert_called_once_with()
|
|
|
|
@patch("sceptre.environment.threading.Event")
|
|
def test_get_threading_events(self, mock_Event):
|
|
mock_stack = Mock()
|
|
mock_stack.name = "name"
|
|
|
|
self.environment.stacks = {"mock_stack": mock_stack}
|
|
|
|
mock_Event.return_value = sentinel.event
|
|
|
|
response = self.environment._get_threading_events()
|
|
assert response == {
|
|
"name": sentinel.event
|
|
}
|
|
|
|
def test_get_initial_statuses(self):
|
|
mock_stack = Mock()
|
|
mock_stack.name = "name"
|
|
|
|
self.environment.stacks = {"name": mock_stack}
|
|
|
|
response = self.environment._get_initial_statuses()
|
|
assert response == {
|
|
"name": StackStatus.PENDING
|
|
}
|
|
|
|
def test_get_launch_dependencies(self):
|
|
mock_stack = Mock()
|
|
mock_stack.name = "dev/mock_stack"
|
|
mock_stack.dependencies = [
|
|
"dev/vpc",
|
|
"dev/subnets",
|
|
"prod/sg"
|
|
]
|
|
|
|
self.environment.stacks = {"mock_stack": mock_stack}
|
|
|
|
response = self.environment._get_launch_dependencies("dev")
|
|
|
|
# Note that "prod/sg" is filtered out, because it's not under the
|
|
# top level environment path "dev".
|
|
assert response == {
|
|
"dev/mock_stack": ["dev/vpc", "dev/subnets"]
|
|
}
|
|
|
|
@patch("sceptre.environment.Environment._get_launch_dependencies")
|
|
def test_get_delete_dependencies(self, mock_get_launch_dependencies):
|
|
mock_get_launch_dependencies.return_value = {
|
|
"dev/mock_stack_1": [],
|
|
"dev/mock_stack_2": [],
|
|
"dev/mock_stack_3": ["dev/mock_stack_1", "dev/mock_stack_2"],
|
|
}
|
|
|
|
dependencies = self.environment._get_delete_dependencies()
|
|
assert dependencies == {
|
|
"dev/mock_stack_1": ["dev/mock_stack_3"],
|
|
"dev/mock_stack_2": ["dev/mock_stack_3"],
|
|
"dev/mock_stack_3": [],
|
|
}
|
|
|
|
def test_check_for_circular_dependencies_with_circular_dependencies(self):
|
|
dependencies = {
|
|
"stack-1": ["stack-2"],
|
|
"stack-2": ["stack-1"]
|
|
}
|
|
|
|
with pytest.raises(CircularDependenciesError):
|
|
self.environment._check_for_circular_dependencies(dependencies)
|
|
|
|
def test_check_for_circular_dependencies_without_find_dependencies(self):
|
|
dependencies = {
|
|
"stack-1": ["stack-2"],
|
|
"stack-2": []
|
|
}
|
|
|
|
# Check this runs without throwing an exception
|
|
self.environment._check_for_circular_dependencies(dependencies)
|
|
|
|
@patch("sceptre.environment.Config")
|
|
def test_get_config(self, mock_Config):
|
|
mock_Config.return_value.read.return_value = {}
|
|
|
|
self.environment.sceptre_dir = "sceptre_dir"
|
|
self.environment.path = "environment_path"
|
|
self.environment._options = {
|
|
"cli_option": "value",
|
|
"user_variables": sentinel.user_variables
|
|
}
|
|
|
|
self.environment._get_config()
|
|
mock_Config.assert_called_once_with(
|
|
sceptre_dir="sceptre_dir",
|
|
environment_path="environment_path",
|
|
base_file_name="config"
|
|
)
|
|
mock_Config.return_value.read.assert_called_once_with(
|
|
sentinel.user_variables
|
|
)
|
|
|
|
def test_get_available_stacks(self):
|
|
self.environment.path = os.path.join(
|
|
"account", "environment", "region"
|
|
)
|
|
self.environment.sceptre_dir = os.path.join(
|
|
os.getcwd(), "tests", "fixtures"
|
|
)
|
|
response = self.environment._get_available_stacks()
|
|
assert sorted(response) == sorted([
|
|
"account/environment/region/vpc",
|
|
"account/environment/region/subnets",
|
|
"account/environment/region/security_groups"
|
|
])
|
|
|
|
@patch("sceptre.environment.Stack")
|
|
@patch("sceptre.environment.Environment._get_available_stacks")
|
|
@patch("sceptre.environment.ConnectionManager")
|
|
@patch("sceptre.environment.Environment._get_config")
|
|
def test_load_stacks(
|
|
self, mock_get_config, mock_ConnectionManager,
|
|
mock_get_available_stacks, mock_Stack
|
|
):
|
|
mock_config = {
|
|
"region": sentinel.region,
|
|
"iam_role": sentinel.iam_role
|
|
}
|
|
mock_get_config.return_value = mock_config
|
|
mock_ConnectionManager.return_value = sentinel.connection_manager
|
|
mock_get_available_stacks.return_value = ["stack_name"]
|
|
mock_Stack.return_value = sentinel.stack
|
|
|
|
response = self.environment._load_stacks()
|
|
|
|
# Check ConnectionManager() is called with correct arguments
|
|
mock_ConnectionManager.assert_called_once_with(
|
|
region=sentinel.region,
|
|
iam_role=sentinel.iam_role
|
|
)
|
|
|
|
# Check Stack() is called with correct arguments
|
|
mock_Stack.assert_called_once_with(
|
|
name="stack_name",
|
|
environment_config=mock_config,
|
|
connection_manager=sentinel.connection_manager
|
|
)
|
|
# Check _load_stacks() returns list of stacks
|
|
assert response == {'stack_name': sentinel.stack}
|
|
|
|
def test_get_available_environments(self):
|
|
self.environment.path = "account"
|
|
self.environment.sceptre_dir = os.path.join(
|
|
os.getcwd(), "tests", "fixtures"
|
|
)
|
|
response = self.environment._get_available_environments()
|
|
assert response == ["account/environment"]
|
|
|
|
@patch("sceptre.environment.Environment")
|
|
@patch("sceptre.environment.Environment._get_available_environments")
|
|
def test_load_environments(
|
|
self, mock_get_available_environments, mock_Environment
|
|
):
|
|
mock_get_available_environments.return_value = ["env"]
|
|
mock_Environment.return_value = sentinel.environment
|
|
|
|
response = self.environment._load_environments()
|
|
assert response == {"env": sentinel.environment}
|