ebcli/controllers/initialize.py (442 lines of code) (raw):
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os.path
from cement.utils.misc import minimal_logger
from ebcli.core import fileoperations, io
from ebcli.core.abstractcontroller import AbstractBaseController
from ebcli.core.ebglobals import Constants
from ebcli.lib import utils, elasticbeanstalk, codecommit, aws
from ebcli.objects.sourcecontrol import SourceControl
from ebcli.objects.platform import PlatformBranch, PlatformVersion
from ebcli.objects.solutionstack import SolutionStack
from ebcli.objects import solutionstack
from ebcli.operations import statusops
from ebcli.objects.exceptions import (
InvalidProfileError,
NoRegionError,
NotInitializedError,
ServiceError,
ValidationError,
)
from ebcli.operations import (
commonops,
gitops,
initializeops,
platformops,
solution_stack_ops,
sshops,
)
from ebcli.operations.tagops import tagops
from ebcli.resources.strings import strings, flag_text, prompts, alerts
LOG = minimal_logger(__name__)
class InitController(AbstractBaseController):
class Meta:
label = 'init'
description = strings['init.info']
arguments = [
(['application_name'], dict(
help=flag_text['init.name'], nargs='?', default=[])),
(['-e', '--environment-name'], dict(help=flag_text['init.environment_name'])),
(['-m', '--modules'], dict(help=flag_text['init.module'], nargs='*')),
(['-p', '--platform'], dict(help=flag_text['init.platform'])),
(['-k', '--keyname'], dict(help=flag_text['init.keyname'])),
(['-i', '--interactive'], dict(
action='store_true', help=flag_text['init.interactive'])),
(['--source'], dict(help=flag_text['init.source'])),
(['--tags'], dict(help=flag_text['create.tags']))
]
usage = 'eb init <application_name> [options ...]'
epilog = strings['init.epilog']
def do_command(self):
commonops.raise_if_inside_platform_workspace()
interactive = self.app.pargs.interactive
region_name = self.app.pargs.region
noverify = self.app.pargs.no_verify_ssl
keyname = self.app.pargs.keyname
profile = self.app.pargs.profile
platform = self.app.pargs.platform
source = self.app.pargs.source
app_name = self.app.pargs.application_name
env_name = self.app.pargs.environment_name
modules = self.app.pargs.modules
force_non_interactive = _customer_is_avoiding_interactive_flow(
self.app.pargs)
tags = self.app.pargs.tags
# The user specifies directories to initialize
if modules and len(modules) > 0:
self.initialize_multiple_directories(
modules,
app_name,
region_name,
interactive,
force_non_interactive,
keyname,
profile,
noverify,
platform
)
return
fileoperations.touch_config_folder()
region_name = commonops.set_region_for_application(interactive, region_name, force_non_interactive, platform)
commonops.set_up_credentials(profile, region_name, interactive)
app_name = get_app_name(app_name, interactive, force_non_interactive)
default_env = env_name or set_default_env(interactive, force_non_interactive)
tags = tagops.get_and_validate_tags(tags)
platform_arn, keyname_of_existing_application = create_app_or_use_existing_one(app_name, default_env, tags)
platform = _determine_platform(
customer_provided_platform=platform,
existing_app_platform=platform_arn,
force_interactive=interactive and not force_non_interactive)
handle_buildspec_image(platform, force_non_interactive)
prompt_codecommit = should_prompt_customer_to_opt_into_codecommit(
force_non_interactive,
source
)
repository, branch = None, None
if prompt_codecommit:
repository, branch = configure_codecommit(source)
initializeops.setup(app_name, region_name, platform, dir_path=None, repository=repository, branch=branch)
configure_keyname(platform, keyname, keyname_of_existing_application, interactive, force_non_interactive)
fileoperations.write_config_setting('global', 'include_git_submodules', True)
if noverify:
fileoperations.write_config_setting('global', 'no-verify-ssl', True)
def initialize_multiple_directories(
self,
modules,
app_name,
region,
interactive,
force_non_interactive,
keyname,
profile,
noverify,
platform
):
application_created = False
cwd = os.getcwd()
for module in modules:
if os.path.exists(module) and os.path.isdir(module):
os.chdir(module)
fileoperations.touch_config_folder()
# Region should be set once for all modules
region = region or commonops.set_region_for_application(interactive, region, force_non_interactive)
commonops.set_up_credentials(profile, region, interactive)
# App name should be set once for all modules
if not app_name:
# Switching back to the root dir will suggest the root dir name
# as the application name
os.chdir(cwd)
app_name = get_app_name(None, interactive, force_non_interactive)
os.chdir(module)
if noverify:
fileoperations.write_config_setting('global', 'no-verify-ssl', True)
default_env = '/ni' if force_non_interactive else None
if not application_created:
platform_arn, keyname_of_existing_application = commonops.create_app(
app_name,
default_env=default_env
)
application_created = True
else:
platform_arn, keyname_of_existing_application = commonops.pull_down_app_info(
app_name,
default_env=default_env
)
io.echo('\n--- Configuring module: {0} ---'.format(module))
module_platform = _determine_platform(
customer_provided_platform=platform,
existing_app_platform=platform_arn,
force_interactive=interactive)
initializeops.setup(app_name, region, module_platform)
configure_keyname(module_platform, keyname, keyname_of_existing_application, interactive, force_non_interactive)
os.chdir(cwd)
def _get_application_name_interactive():
app_list = elasticbeanstalk.get_application_names()
file_name = fileoperations.get_current_directory_name()
new_app = False
if len(app_list) > 0:
io.echo()
io.echo('Select an application to use')
new_app_option = '[ Create new Application ]'
app_list.append(new_app_option)
try:
default_option = app_list.index(file_name) + 1
except ValueError:
default_option = len(app_list)
app_name = utils.prompt_for_item_in_list(app_list,
default=default_option)
if app_name == new_app_option:
new_app = True
if len(app_list) == 0 or new_app:
io.echo()
io.echo('Enter Application Name')
unique_name = utils.get_unique_name(file_name, app_list)
app_name = io.prompt_for_unique_name(unique_name, app_list)
return app_name
# Code Commit repository setup methods
def get_repository_interactive():
source_control = SourceControl.get_source_control()
# Give list of code commit repositories to use
new_repo = False
repo_list = codecommit.list_repositories()["repositories"]
current_repository = source_control.get_current_repository()
current_repository = current_repository or fileoperations.get_current_directory_name()
# If there are existing repositories prompt the user to pick one
# otherwise set default as the file name
if len(repo_list) > 0:
repo_list = list(map(lambda r: r["repositoryName"], repo_list))
io.echo()
io.echo('Select a repository')
new_repo_option = '[ Create new Repository ]'
repo_list.append(new_repo_option)
try:
default_option = repo_list.index(current_repository) + 1
except ValueError:
default_option = len(repo_list)
repo_name = utils.prompt_for_item_in_list(repo_list,
default=default_option)
if repo_name == new_repo_option:
new_repo = True
# Create a new repository if the user specifies or there are no existing repositories
if len(repo_list) == 0 or new_repo:
io.echo()
io.echo('Enter Repository Name')
unique_name = utils.get_unique_name(current_repository, repo_list)
repo_name = io.prompt_for_unique_name(unique_name, repo_list)
create_codecommit_repository(repo_name)
return repo_name
def create_codecommit_repository(repo_name):
# Create the repository if we get here
codecommit.create_repository(repo_name, "Created with EB CLI")
io.echo("Successfully created repository: {0}".format(repo_name))
def setup_codecommit_remote_repo(repository, source_control):
result = codecommit.get_repository(repository)
remote_url = result['repositoryMetadata']['cloneUrlHttp']
source_control.setup_codecommit_remote_repo(remote_url=remote_url)
def create_codecommit_branch(source_control, branch_name):
current_commit = source_control.get_current_commit()
# Creating the branch requires that we setup the remote branch first
# to ensure the code commit branch is synced with the local branch
if current_commit is None:
# TODO: Test on windows for weird empty returns with the staged files
staged_files = source_control.get_list_of_staged_files()
if not staged_files:
source_control.create_initial_commit()
else:
LOG.debug("Cannot create placeholder commit because there are staged files: {0}".format(staged_files))
io.echo("Could not set create a commit with staged files; cannot setup CodeCommit branch without a commit")
return None
source_control.setup_new_codecommit_branch(branch_name=branch_name)
io.echo("Successfully created branch: {0}".format(branch_name))
def get_branch_interactive(repository):
source_control = SourceControl.get_source_control()
# Give list of code commit branches to use
new_branch = False
branch_list = codecommit.list_branches(repository)["branches"]
current_branch = source_control.get_current_branch()
# If there are existing branches prompt the user to pick one
if len(branch_list) > 0:
io.echo('Select a branch')
new_branch_option = '[ Create new Branch with local HEAD ]'
branch_list.append(new_branch_option)
try:
default_option = branch_list.index(current_branch) + 1
except ValueError:
default_option = len(branch_list)
branch_name = utils.prompt_for_item_in_list(branch_list,
default=default_option)
if branch_name == new_branch_option:
new_branch = True
# Create a new branch if the user specifies or there are no existing branches
if len(branch_list) == 0 or new_branch:
new_branch = True
io.echo()
io.echo('Enter Branch Name')
io.echo('***** Must have at least one commit to create a new branch with CodeCommit *****')
unique_name = utils.get_unique_name(current_branch, branch_list)
branch_name = io.prompt_for_unique_name(unique_name, branch_list)
# Setup git to push to this repo
result = codecommit.get_repository(repository)
remote_url = result['repositoryMetadata']['cloneUrlHttp']
source_control.setup_codecommit_remote_repo(remote_url=remote_url)
if len(branch_list) == 0 or new_branch:
LOG.debug("Creating a new branch")
try:
create_codecommit_branch(source_control, branch_name)
except ServiceError:
io.echo("Could not set CodeCommit branch with the current commit, run with '--debug' to get the full error")
return None
elif not new_branch:
LOG.debug("Setting up an existing branch")
succesful_branch = source_control.setup_existing_codecommit_branch(branch_name)
if not succesful_branch:
io.echo("Could not set CodeCommit branch, run with '--debug' to get the full error")
return None
return branch_name
def configure_codecommit(source):
source_location, repository, branch = utils.parse_source(source)
source_control = SourceControl.get_source_control()
if not source_location:
should_continue = io.get_boolean_response(text=prompts['codecommit.usecc'], default=True)
if not should_continue:
LOG.debug("Denied option to use CodeCommit, continuing initialization")
return repository, branch
# Setup git config settings for code commit credentials
source_control.setup_codecommit_cred_config()
repository, branch = establish_codecommit_repository_and_branch(repository, branch, source_control, source_location)
return repository, branch
def configure_keyname(solution, keyname, keyname_of_existing_app, interactive, force_non_interactive):
if 'IIS' not in solution:
keyname = get_keyname(keyname, keyname_of_existing_app, interactive, force_non_interactive)
if keyname == -1:
keyname = None
fileoperations.write_config_setting(
'global',
'default_ec2_keyname',
keyname
)
def create_app_or_use_existing_one(app_name, default_env, tags):
if elasticbeanstalk.application_exist(app_name):
return commonops.pull_down_app_info(app_name, default_env=default_env)
else:
return commonops.create_app(app_name, default_env=default_env, tags=tags)
def directory_is_already_associated_with_a_branch():
return gitops.git_management_enabled()
def extract_solution_stack_from_env_yaml():
env_yaml_platform = fileoperations.get_platform_from_env_yaml()
if env_yaml_platform:
platform = solutionstack.SolutionStack(env_yaml_platform).platform_shorthand
return platform
def get_app_name(customer_specified_app_name, interactive, force_non_interactive):
if customer_specified_app_name:
return customer_specified_app_name
try:
app_name = fileoperations.get_application_name(default=None)
except NotInitializedError:
app_name = None
if force_non_interactive and not interactive:
return fileoperations.get_current_directory_name()
elif interactive or not app_name:
return _get_application_name_interactive()
return app_name
def get_keyname(keyname, keyname_of_existing_app, interactive, force_non_interactive):
keyname_passed_through_command_line = not not keyname
keyname = keyname or keyname_of_existing_app
if not keyname:
try:
keyname = commonops.get_default_keyname()
except NotInitializedError:
keyname = None
if force_non_interactive and not interactive:
return keyname
if (
(interactive and not keyname_passed_through_command_line)
or (not keyname and not force_non_interactive)
):
keyname = sshops.prompt_for_ec2_keyname()
elif keyname != -1:
commonops.upload_keypair_if_needed(keyname)
return keyname
def handle_buildspec_image(solution, force_non_interactive):
if not fileoperations.build_spec_exists():
return None
build_spec = fileoperations.get_build_configuration()
if not force_non_interactive and build_spec and build_spec.image is None:
LOG.debug("Buildspec file is present but image does not exist. Attempting to fill best guess.")
platform_image = initializeops.get_codebuild_image_from_platform(solution)
if not platform_image:
io.echo("No images found for platform: {platform}".format(platform=solution))
return None
if isinstance(platform_image[0], dict):
if len(platform_image) == 1:
io.echo(strings['codebuild.latestplatform'].replace('{platform}', solution))
selected_image = platform_image[0]
else:
io.echo(prompts['codebuild.getplatform'].replace('{platform}', solution))
selected = int(utils.prompt_for_index_in_list([image['description'] for image in platform_image]))
if selected is not None and 0 <= selected < len(platform_image):
selected_description = platform_image[selected]['description']
else:
LOG.error("Invalid selection.")
return None
matching_images = [image for image in platform_image if selected_description == image['description']]
if not matching_images:
LOG.error(f"No matching images found for selected description: {selected}")
return None
selected_image = matching_images[0]
fileoperations.write_buildspec_config_header('Image', selected_image['name'])
return None
def set_default_env(interactive, force_non_interactive):
if force_non_interactive:
return '/ni'
if not interactive:
try:
return commonops.get_current_branch_environment()
except NotInitializedError:
pass
def establish_codecommit_branch(repository, branch, source_control, source_location):
if branch is None:
branch = get_branch_interactive(repository)
else:
try:
codecommit.get_branch(repository, branch)
except ServiceError as ex:
if source_location:
create_codecommit_branch(source_control, branch)
else:
io.log_error(strings['codecommit.nobranch'])
raise ex
source_control.setup_existing_codecommit_branch(branch)
return branch
def establish_codecommit_repository(repository, source_control, source_location):
if repository is None:
repository = get_repository_interactive()
else:
try:
setup_codecommit_remote_repo(repository, source_control)
except ServiceError as ex:
if source_location:
create_codecommit_repository(repository)
setup_codecommit_remote_repo(repository, source_control)
else:
io.log_error(strings['codecommit.norepo'])
raise ex
return repository
def establish_codecommit_repository_and_branch(repository, branch, source_control, source_location):
repository = establish_codecommit_repository(repository, source_control, source_location)
branch = establish_codecommit_branch(repository, branch, source_control, source_location)
return repository, branch
def should_prompt_customer_to_opt_into_codecommit(
force_non_interactive,
source
):
source_location, repository, branch = utils.parse_source(source)
if force_non_interactive:
return False
elif not codecommit.region_supported():
if source_location:
io.log_warning(strings['codecommit.badregion'])
return False
elif not fileoperations.is_git_directory_present():
return False
elif not fileoperations.program_is_installed('git'):
return False
elif directory_is_already_associated_with_a_branch():
return False
return True
def _customer_is_avoiding_interactive_flow(command_args):
return not not command_args.platform
def _determine_platform(
customer_provided_platform=None,
existing_app_platform=None,
force_interactive=False,
):
platform = None
if not force_interactive:
if customer_provided_platform:
platform = platformops.get_platform_for_platform_string(
customer_provided_platform)
if not platform:
try:
platform = platformops.get_configured_default_platform()
except NotInitializedError:
# If the directory is not initialized we can safely continue
# to get the platform from other sources
pass
if existing_app_platform and not platform:
platform = PlatformVersion(existing_app_platform).hydrate(
elasticbeanstalk.describe_platform_version)
if not platform and fileoperations.env_yaml_exists():
platform = extract_solution_stack_from_env_yaml()
if platform:
io.echo(strings['init.usingenvyamlplatform'].replace('{platform}', platform))
if not platform:
platform = platformops.prompt_for_platform()
if isinstance(platform, PlatformVersion):
statusops.alert_platform_status(platform)
if customer_provided_platform == platform.platform_arn:
return platform.platform_arn
return platform.platform_branch_name or platform.platform_name
if isinstance(platform, PlatformBranch):
statusops.alert_platform_branch_status(platform)
return platform.branch_name
if isinstance(platform, SolutionStack):
return platform.platform_shorthand
return platform