def test_pull_repo_https_credentials(self, mock_check_output, mock_check_call): """ Tests the pull_repo by mocking the subprocess.check_output to return git http credentials. Args: mock_check_output: MagicMock, returns valid git responses in order of being called, with the repo coming from the ef_site_config.py Returns: None Raises: AssertionError if any of the assert checks fail """ mock_check_output.side_effect = [ "origin\thttps://" + EFConfig.EF_REPO + ".git", EFConfig.EF_REPO_BRANCH ] try: ef_conf_utils.pull_repo() except RuntimeError as exception: self.fail( "Exception occurred during test_pull_repo_ssh_credentials: " + exception.message)
def main(): # Fetch args and load context context = handle_args_and_set_context(sys.argv[1:]) # Refresh from repo if necessary and possible (gets don't need service registry, sets do) if (context.rollback or context.value) and not (context.devel or getenv( "JENKINS_URL", False)): print("Refreshing repo") try: pull_repo() except RuntimeError as error: fail("Error checking or pulling repo", error) # Sign on to AWS and create clients if context.whereami in ["ec2"]: # Always use instance credentials in EC2. One day we'll have "lambda" in there too, so use "in" w/ list aws_session_alias = None else: # Otherwise use local user credential matching the account alias aws_session_alias = context.account_alias # Make AWS clients try: context.set_aws_clients( create_aws_clients(EFConfig.DEFAULT_REGION, aws_session_alias, "ec2", "s3", "sts")) except RuntimeError: fail( "Exception creating AWS client in region {} with aws account alias {} (None=instance credentials)" .format(EFConfig.DEFAULT_REGION, aws_session_alias)) # Instantiate a versionresolver - we'll use some of its methods context._versionresolver = EFVersionResolver(context.aws_client()) # Carry out the requested action if context.get: cmd_get(context) elif context.history: cmd_history(context) elif context.rollback: cmd_rollback(context) elif context.rollback_to: cmd_rollback_to(context) elif context.show: cmd_show(context) elif context.value: cmd_set(context)
def test_pull_repo_incorrect_repo(self, mock_check_output): """ Tests pull_repo to see if it throws an exception when the supplied repo doesn't match the one in ef_site_config.py Args: mock_check_output: MagicMock, returns git responses with non matching repo names Returns: None Raises: AssertionError if any of the assert checks fail """ mock_check_output.side_effect = [ "[email protected]:company/wrong_repo.git " "[email protected]:company/wrong_repo.git" ] with self.assertRaises(RuntimeError) as exception: ef_conf_utils.pull_repo() self.assertIn("Must be in", exception.exception.message)
def test_pull_repo_incorrect_branch(self, mock_check_output): """ Tests pull_repo to see if it throws an error when the mocked check_output states it's on a branch other than the one specified in ef_site_config.py Args: mock_check_output: MagicMock, returns some valid git responses, with the repo coming from the ef_site_config.py, and then a non matching branch name Returns: None Raises: AssertionError if any of the assert checks fail """ mock_check_output.side_effect = [ "user@" + EFConfig.EF_REPO.replace("/", ":", 1) + ".git", "wrong_branch" ] with self.assertRaises(RuntimeError) as exception: ef_conf_utils.pull_repo() self.assertIn("Must be on branch:", exception.exception.message)
def main(): global CONTEXT, CLIENTS, AWS_RESOLVER CONTEXT = handle_args_and_set_context(sys.argv[1:]) if not (CONTEXT.devel or getenv("JENKINS_URL", False)): try: pull_repo() except RuntimeError as error: fail("Error checking or pulling repo", error) else: print("Not refreshing repo because --devel was set or running on Jenkins") # sign on to AWS and create clients and get account ID try: # If running in EC2, always use instance credentials. One day we'll have "lambda" in there too, so use "in" w/ list if CONTEXT.whereami == "ec2": CLIENTS = create_aws_clients(EFConfig.DEFAULT_REGION, None, "ec2", "iam", "kms") CONTEXT.account_id = str(json.loads(http_get_metadata('iam/info'))["InstanceProfileArn"].split(":")[4]) else: # Otherwise, we use local user creds based on the account alias CLIENTS = create_aws_clients(EFConfig.DEFAULT_REGION, CONTEXT.account_alias, "ec2", "iam", "kms", "sts") CONTEXT.account_id = get_account_id(CLIENTS["sts"]) except RuntimeError: fail("Exception creating AWS clients in region {} with profile {}".format( EFConfig.DEFAULT_REGION, CONTEXT.account_alias)) # Instantiate an AWSResolver to lookup AWS resources AWS_RESOLVER = EFAwsResolver(CLIENTS) # Show where we're working if not CONTEXT.commit: print("=== DRY RUN ===\nUse --commit to create roles and security groups\n=== DRY RUN ===") print("env: {}".format(CONTEXT.env)) print("env_full: {}".format(CONTEXT.env_full)) print("env_short: {}".format(CONTEXT.env_short)) print("aws account profile: {}".format(CONTEXT.account_alias)) print("aws account number: {}".format(CONTEXT.account_id)) # Step through all services in the service registry for CONTEXT.service in CONTEXT.service_registry.iter_services(): service_name = CONTEXT.service[0] target_name = "{}-{}".format(CONTEXT.env, service_name) sr_entry = CONTEXT.service[1] service_type = sr_entry['type'] print_if_verbose("service: {} in env: {}".format(service_name, CONTEXT.env)) # Is this service_type handled by this tool? if service_type not in SUPPORTED_SERVICE_TYPES: print_if_verbose("unsupported service type: {}".format(service_type)) continue # Is the env valid for this service? if CONTEXT.env_full not in CONTEXT.service_registry.valid_envs(service_name): print_if_verbose("env: {} not valid for service {}".format(CONTEXT.env_full, service_name)) continue # Is the service_type allowed in 'global'? if CONTEXT.env == "global" and service_type not in GLOBAL_SERVICE_TYPES: print_if_verbose("env: {} not valid for service type {}".format(CONTEXT.env, service_type)) continue # 1. CONDITIONALLY MAKE ROLE AND/OR INSTANCE PROFILE FOR THE SERVICE # If service gets a role, create with either a custom or default AssumeRole policy document conditionally_create_role(target_name, sr_entry) # Instance profiles and security groups are not allowed in the global scope if CONTEXT.env != "global": conditionally_create_profile(target_name, service_type) # 2. SECURITY GROUP(S) FOR THE SERVICE : only some types of services get security groups conditionally_create_security_groups(CONTEXT.env, service_name, service_type) # 3. KMS KEY FOR THE SERVICE : only some types of services get kms keys conditionally_create_kms_key(target_name, service_type) # 4. ATTACH AWS MANAGED POLICIES TO ROLE conditionally_attach_aws_managed_policies(target_name, sr_entry) # 5. ATTACH CUSTOMER MANAGED POLICIES TO ROLE conditionally_attach_customer_managed_policies(target_name, sr_entry) # 6. INLINE SERVICE'S POLICIES INTO ROLE # only eligible service types with "policies" sections in the service registry get policies conditionally_inline_policies(target_name, sr_entry) print("Exit: success")
def main(): context = handle_args_and_set_context(sys.argv[1:]) if context.changeset: print( "=== CHANGESET ===\nCreating changeset only. See AWS GUI for changeset\n=== CHANGESET ===" ) elif not context.commit: print( "=== DRY RUN ===\nValidation only. Use --commit to push template to CF\n=== DRY RUN ===" ) service_name = os.path.basename(os.path.splitext(context.template_file)[0]) template_file_dir = os.path.dirname(context.template_file) # parameter file may not exist, but compute the name it would have if it did parameter_file_dir = template_file_dir + "/../parameters" parameter_file = parameter_file_dir + "/" + service_name + ".parameters." + context.env_full + ".json" # If running in EC2, use instance credentials (i.e. profile = None) # otherwise, use local credentials with profile name in .aws/credentials == account alias name if context.whereami == "ec2" and not os.getenv("JENKINS_URL", False): profile = None else: profile = context.account_alias # Get service registry and refresh repo if appropriate try: if not (context.devel or os.getenv("JENKINS_URL", False)): pull_repo() else: print( "not refreshing repo because --devel was set or running on Jenkins" ) except Exception as error: fail("Error: ", error) # Service must exist in service registry if context.service_registry.service_record(service_name) is None: fail("service: {} not found in service registry: {}".format( service_name, context.service_registry.filespec)) if not context.env_full in context.service_registry.valid_envs( service_name): fail("Invalid environment: {} for service_name: {}\nValid environments are: {}" \ .format(context.env_full, service_name, ", ".join(context.service_registry.valid_envs(service_name)))) if context.percent and (context.percent <= 0 or context.percent > 100): fail( "Percent value cannot be less than or equal to 0 and greater than 100" ) # Set the region found in the service_registry. Default is EFConfig.DEFAULT_REGION if region key not found region = context.service_registry.service_region(service_name) if context.verbose: print("service_name: {}".format(service_name)) print("env: {}".format(context.env)) print("env_full: {}".format(context.env_full)) print("env_short: {}".format(context.env_short)) print("region: {}".format(region)) print("template_file: {}".format(context.template_file)) print("parameter_file: {}".format(parameter_file)) if profile: print("profile: {}".format(profile)) print("whereami: {}".format(context.whereami)) print("service type: {}".format( context.service_registry.service_record(service_name)["type"])) template = resolve_template(template=context.template_file, profile=profile, env=context.env, region=region, service=service_name, verbose=context.verbose) # Create clients - if accessing by role, profile should be None try: clients = create_aws_clients(region, profile, "cloudformation", "autoscaling") except RuntimeError as error: fail( "Exception creating clients in region {} with profile {}".format( region, profile), error) stack_name = context.env + "-" + service_name try: stack_exists = clients["cloudformation"].describe_stacks( StackName=stack_name) except botocore.exceptions.ClientError: stack_exists = False # Load parameters from file if os.path.isfile(parameter_file): parameters_template = resolve_template(template=parameter_file, profile=profile, env=context.env, region=region, service=service_name, verbose=context.verbose) try: parameters = json.loads(parameters_template) except ValueError as error: fail("JSON error in parameter file: {}".format( parameter_file, error)) else: parameters = [] if context.percent: print("Modifying deploy rate to {}%".format(context.percent)) modify_template = json.loads(template) for key in modify_template["Resources"]: if modify_template["Resources"][key][ "Type"] == "AWS::AutoScaling::AutoScalingGroup": if modify_template["Resources"][key]["UpdatePolicy"]: autoscaling_group = modify_template["Resources"][key][ "Properties"] service = autoscaling_group["Tags"][0]["Value"] autoscaling_group_properties = get_autoscaling_group_properties( clients["autoscaling"], service.split("-")[0], "-".join(service.split("-")[1:])) new_max_batch_size = calculate_max_batch_size( clients["autoscaling"], service, context.percent) modify_template["Resources"][key]["UpdatePolicy"][ "AutoScalingRollingUpdate"][ "MaxBatchSize"] = new_max_batch_size current_desired = autoscaling_group_properties[0][ "DesiredCapacity"] if autoscaling_group_properties else "missing" print( "Service {} [current desired: {}, calculated max batch size: {}]" .format(service, current_desired, new_max_batch_size)) template = json.dumps(modify_template) # Detect if the template exceeds the maximum size that is allowed by Cloudformation if len(template) > CLOUDFORMATION_SIZE_LIMIT: # Compress the generated template by removing whitespaces print( "Template exceeds the max allowed length that Cloudformation will accept. Compressing template..." ) print("Uncompressed size of template: {}".format(len(template))) unpacked = json.loads(template) template = json.dumps(unpacked, separators=(",", ":")) print("Compressed size of template: {}".format(len(template))) # Validate rendered template before trying the stack operation if context.verbose: print("Validating template") try: clients["cloudformation"].validate_template(TemplateBody=template) json.loads( template) # Tests for valid JSON syntax, oddly not handled above except botocore.exceptions.ClientError as error: fail("Template did not pass validation", error) except ValueError as e: # includes simplejson.decoder.JSONDecodeError fail('Failed to decode JSON', e) print("Template passed validation") # DO IT try: if context.changeset: print("Creating changeset: {}".format(stack_name)) results = clients["cloudformation"].create_change_set( StackName=stack_name, TemplateBody=template, Parameters=parameters, Capabilities=[ 'CAPABILITY_AUTO_EXPAND', 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ], ChangeSetName=stack_name, ClientToken=stack_name) if is_stack_termination_protected_env(context.env): enable_stack_termination_protection(clients, stack_name) results_ids = { key: value for key, value in results.iteritems() if key in ('Id', 'StackId') } print("Changeset Info: {}".format(json.dumps(results_ids))) elif context.commit: if stack_exists: print("Updating stack: {}".format(stack_name)) clients["cloudformation"].update_stack( StackName=stack_name, TemplateBody=template, Parameters=parameters, Capabilities=[ 'CAPABILITY_AUTO_EXPAND', 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ]) if is_stack_termination_protected_env(context.env): enable_stack_termination_protection(clients, stack_name) else: print("Creating stack: {}".format(stack_name)) clients["cloudformation"].create_stack( StackName=stack_name, TemplateBody=template, Parameters=parameters, Capabilities=[ 'CAPABILITY_AUTO_EXPAND', 'CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM' ]) if is_stack_termination_protected_env(context.env): enable_stack_termination_protection(clients, stack_name) if context.poll_status: while True: stack_status = clients["cloudformation"].describe_stacks( StackName=stack_name)["Stacks"][0]["StackStatus"] if context.verbose: print("{}".format(stack_status)) if stack_status.endswith('ROLLBACK_COMPLETE'): print( "Stack went into rollback with status: {}".format( stack_status)) sys.exit(1) elif re.match(r".*_COMPLETE(?!.)", stack_status) is not None: break elif re.match(r".*_FAILED(?!.)", stack_status) is not None: print("Stack failed with status: {}".format( stack_status)) sys.exit(1) elif re.match(r".*_IN_PROGRESS(?!.)", stack_status) is not None: time.sleep(EFConfig.EF_CF_POLL_PERIOD) elif context.lint: tester = CFTemplateLinter(template) tester.run_tests() exit(tester.exit_code) except botocore.exceptions.ClientError as error: if error.response["Error"][ "Message"] in "No updates are to be performed.": # Don't fail when there is no update to the stack print("No updates are to be performed.") else: fail("Error occurred when creating or updating stack", error)