Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

K-80: tf state file handling for multiple deployments #95

Merged
merged 10 commits into from
Feb 4, 2020
67 changes: 30 additions & 37 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.3
1.1.4
32 changes: 21 additions & 11 deletions cli/kaos_cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from kaos_cli.facades.backend_facade import BackendFacade, is_cloud_provider
from typing import Optional

from kaos_cli.utils.helpers import build_dir
from kaos_cli.utils.custom_classes import CustomHelpOrder, NotRequiredIf
from kaos_cli.utils.decorators import build_env_check, pass_obj
from kaos_cli.utils.validators import validate_unused_port, validate_inputs, EnvironmentState
Expand All @@ -16,8 +17,13 @@
# BUILD group
# =============
@click.group(name='build', cls=CustomHelpOrder,
short_help=' {} and its {} '.format(
click.style('build', bold=True), click.style('sub-commands', bold=True)))
short_help='{} and its {}: {}, {}, {} and {}'.format(
click.style('Infrastructure deployments', bold=True),
click.style('sub-commands', bold=False),
click.style('deploy', bold=True),
click.style('list', bold=True),
click.style('set', bold=True),
click.style('active', bold=True)))
def build():
"""
Build command allows you to deploy infrastructre and list the available deployments
Expand All @@ -26,8 +32,9 @@ def build():


@build.command(name='deploy',
short_help='{}'.format(
click.style('Build the kaos backend', bold=True, fg='black')))
short_help='{} the {} backend'.format(
click.style('Build', bold=True),
click.style('kaos', bold=True)))
@click.option('-c', '--cloud', type=click.Choice([DOCKER, MINIKUBE, AWS, GCP]),
help='selected provider', required=True)
@click.option('-e', '--env', type=click.Choice(['prod', 'stage', 'dev']),
Expand Down Expand Up @@ -58,9 +65,12 @@ def deploy(backend: BackendFacade, cloud: str, env: str, force: bool, verbose: b
click.echo('{} - Performing {} build of the backend'.format(
click.style("Warning", bold=True, fg='yellow'),
click.style("force", bold=True)))
env_state.remove_terraform_files()

# set env variable appropriately
env_state.set_build_env()
cloud = env_state.cloud
env = env_state.env

if not yes:
# confirm creation of backend
Expand Down Expand Up @@ -106,7 +116,6 @@ def deploy(backend: BackendFacade, cloud: str, env: str, force: bool, verbose: b
sys.exit(1)

try:

is_built_successfully, env_state = backend.build(env_state.cloud,
env_state.env,
local_backend=local_backend,
Expand All @@ -129,7 +138,7 @@ def deploy(backend: BackendFacade, cloud: str, env: str, force: bool, verbose: b
click.echo("{} - Successfully built {} [{}] environment".format(
click.style("Info", bold=True, fg='green'),
click.style('kaos', bold=True),
click.style(env_state.env, bold=True, fg='blue')))
click.style(env, bold=True, fg='blue')))
else:
click.echo("{} - Successfully built {} environment".format(
click.style("Info", bold=True, fg='green'),
Expand All @@ -139,8 +148,8 @@ def deploy(backend: BackendFacade, cloud: str, env: str, force: bool, verbose: b
click.echo("{} - Deployment Unsuccessful while creating {} [{} {}] environment".format(
click.style("Error", bold=True, fg='red'),
click.style('kaos', bold=True),
click.style(env_state.cloud, bold=True, fg='red'),
click.style(env_state.env, bold=True, fg='red'))),
click.style(cloud, bold=True, fg='red'),
click.style(env, bold=True, fg='red'))),
sys.exit(1)

except Exception as e:
Expand Down Expand Up @@ -250,8 +259,9 @@ def get_active_context(backend: BackendFacade):


@click.command(name='destroy',
short_help='{}'.format(
click.style('Destroy the kaos backend', bold=True, fg='black')))
short_help='{} the {} backend'.format(
click.style('Destroys', bold=True),
click.style('kaos', bold=True)))
@click.option('-c', '--cloud', type=click.Choice([DOCKER, MINIKUBE, AWS, GCP]),
help='selected provider provider', required=True)
@click.option('-e', '--env', type=click.Choice(['prod', 'stage', 'dev']),
Expand All @@ -272,7 +282,7 @@ def destroy(backend: BackendFacade, cloud, env, verbose, yes):
env_state.set_build_env()

# Ensure that appropriate warnings are displayed
env_state.validate_if_tfstate_exits()
env_state.validate_if_tf_state_exits()

if not yes:
# confirm creation of backend
Expand Down
77 changes: 35 additions & 42 deletions cli/kaos_cli/facades/backend_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,14 @@
from distutils.dir_util import copy_tree

import requests
from kaos_cli.constants import DOCKER, MINIKUBE, PROVIDER_DICT, AWS, BACKEND, INFRASTRUCTURE, GCP, LOCAL_CONFIG_DICT, \
from kaos_cli.constants import DOCKER, MINIKUBE, AWS, BACKEND, INFRASTRUCTURE, GCP, LOCAL_CONFIG_DICT, \
CONTEXTS, ACTIVE, BACKEND_CACHE, DEFAULT, USER, REMOTE, KAOS_STATE_DIR
from kaos_cli.exceptions.exceptions import HostnameError
from kaos_cli.services.state_service import StateService
from kaos_cli.services.terraform_service import TerraformService
from kaos_cli.utils.environment import check_environment
from kaos_cli.utils.helpers import build_dir
from kaos_cli.utils.validators import EnvironmentState
from kaos_cli.exceptions.handle_exceptions import handle_specific_exception, handle_exception


def is_cloud_provider(cloud):
return cloud not in (DOCKER, MINIKUBE)
from kaos_cli.utils.validators import EnvironmentState, is_cloud_provider


class BackendFacade:
Expand Down Expand Up @@ -134,17 +129,18 @@ def set_context_by_index(self, index):

def build(self, provider, env, local_backend=False, verbose=False):
env_state = EnvironmentState.initialize(provider, env)
if not env_state.if_build_dir_exists:

if not os.path.exists(env_state.build_dir):
build_dir(env_state.build_dir)

auth_token = uuid.uuid4()
extra_vars = self._get_vars(provider, env_state.build_dir, auth_token)
self.tf_service.cd_dir(env_state.build_dir)

self.tf_service.cd_dir(env_state.build_dir)
self.tf_service.set_verbose(verbose)
directory = self._tf_init(provider, env, local_backend, destroying=False)
self.tf_service.plan(directory, extra_vars)
self.tf_service.apply(directory, extra_vars)
self._tf_init(env_state, provider, env, local_backend, destroying=False)
self.tf_service.plan(env_state.build_dir, extra_vars)
self.tf_service.apply(env_state.build_dir, extra_vars)
self.tf_service.execute()

# check if the deployed successfully
Expand All @@ -153,20 +149,15 @@ def build(self, provider, env, local_backend=False, verbose=False):

if env_state.if_tfstate_exists:
url, kubeconfig = self._parse_config(env_state.build_dir)

current_context = provider if provider in [DOCKER, MINIKUBE] else f"{provider}_{env}"

self.state_service.set(DEFAULT, user=USER)

self._set_context_list(current_context)
self._set_active_context(current_context)
self.state_service.set(current_context)

self.state_service.set_section(current_context, BACKEND,
url=url, token=auth_token)
self.state_service.set_section(current_context, INFRASTRUCTURE,
kubeconfig=kubeconfig)

self.state_service.write()
return True, env_state

Expand All @@ -177,14 +168,17 @@ def destroy(self, env_state, verbose=False):
self.tf_service.cd_dir(env_state.build_dir)

self.tf_service.set_verbose(verbose)
directory = self._tf_init(env_state.cloud, env_state.env, local_backend=False, destroying=True)

self._tf_init(env_state, env_state.cloud, env_state.env, local_backend=False, destroying=True)

current_context = env_state.cloud if env_state.cloud in [DOCKER, MINIKUBE] \
else env_state.cloud + '_' + env_state.env

self._delete_resources(current_context)
self._unset_context_list(current_context)
self._remove_section(current_context)
self._deactivate_context()
self.tf_service.destroy(directory, extra_vars)
self.tf_service.destroy(env_state.build_dir, extra_vars)
self.tf_service.execute()
self._remove_build_files(env_state.build_dir)
self.state_service.write()
Expand All @@ -207,25 +201,21 @@ def _delete_resources(self, context):
if self.state_service.has_section(context, BACKEND):
requests.delete(f"{self.url}/internal/resources")

def _tf_init(self, provider, env, local_backend, destroying=False):
directory = PROVIDER_DICT.get(provider)
def _tf_init(self, env_state, provider, env, local_backend, destroying=False):
check_environment(provider)
if is_cloud_provider(provider):
provider_directory = f"{directory}/{env}"
directory = f"{directory}/__working_{env}"
if not destroying or not os.path.isdir(directory):
copy_tree(provider_directory, directory)
if not destroying or not os.path.isdir(env_state.build_dir):
copy_tree(env_state.provider_directory, env_state.build_dir)
if local_backend:
shutil.copy(LOCAL_CONFIG_DICT.get(provider), directory)
shutil.copy(LOCAL_CONFIG_DICT.get(provider), env_state.build_dir)

# simply always create the workspace
self.tf_service.init(directory)
self.tf_service.new_workspace(directory, env)
self.tf_service.select_workspace(directory, env)

self.tf_service.init(env_state.build_dir)
self.tf_service.new_workspace(env_state.build_dir, env)
self.tf_service.select_workspace(env_state.build_dir, env)
else:
self.tf_service.init(directory)
return directory
copy_tree(env_state.provider_directory, env_state.build_dir)
self.tf_service.init(env_state.build_dir)

def _set_context_list(self, current_context):
try:
Expand All @@ -236,17 +226,20 @@ def _set_context_list(self, current_context):
updated_contexts = []

if isinstance(contexts, list):
contexts.append(current_context)
if current_context not in contexts:
contexts.append(current_context)
updated_contexts = contexts
elif isinstance(contexts, str) or not contexts:
# There is only one context or no context in available contexts
if contexts:
# exactly one available context
updated_contexts.append(contexts)
updated_contexts.append(current_context)
else:
# no available context
updated_contexts.append(current_context)
# check if current context is the same as existing context
if current_context != contexts:
# There is only one context or no context in available contexts
if contexts:
# exactly one available context
updated_contexts.append(contexts)
updated_contexts.append(current_context)
else:
# no available context
updated_contexts.append(current_context)

self.state_service.set(CONTEXTS, environments=updated_contexts)

Expand Down Expand Up @@ -311,7 +304,7 @@ def _parse_config(dir_build):

@staticmethod
def _get_vars(provider, dir_build, auth_token=None):
extra_vars = f"--var config_dir={dir_build} --var token={auth_token}"
extra_vars = f"--var config_dir={dir_build} --var token={auth_token} "

if provider == AWS:
KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
Expand Down
Loading