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

#3: Added release code build #5

Merged
merged 29 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
77a60e4
#3: Added release code build
tomuben May 4, 2022
3a6ef95
Added unit test for start_release_build
tomuben May 4, 2022
7d47479
Fixed import of new commands
tomuben May 4, 2022
a53a9cd
Fix template for release build
tomuben May 4, 2022
e94b347
Fixed default output path for release buildspecs
tomuben May 5, 2022
cf53a0f
Added generate-release-buildspecs to import list
tomuben May 5, 2022
8891100
Added start_release_build to import list
tomuben May 5, 2022
9a91a1a
Added build_status to exception
tomuben May 5, 2022
37783c5
Fix match name of release CodeBuild
tomuben May 5, 2022
8ec6071
Changed logic for getting release code build:
tomuben May 5, 2022
e2c8f41
Changed comparison
tomuben May 5, 2022
2d2e859
Check for timeout
tomuben May 5, 2022
8855a27
Splitted release-build into version for release and release-test
tomuben May 5, 2022
e240b4d
Made cmd line parameter aws_profile optional
tomuben May 5, 2022
7f48824
Command start_test_release_build now creates the test draft release o…
tomuben May 6, 2022
d3391a2
Added start_test_release_build to import
tomuben May 6, 2022
c0d8cb9
Fixed start_test_release_build t
tomuben May 6, 2022
9ca5853
Fixed release_build_buildspec.yaml
tomuben May 6, 2022
c5587ac
Sort flavors in buildspec
tomuben May 6, 2022
20cde9d
Fix bug in generate_buildspec_common.py
tomuben May 6, 2022
77bceb4
Added some additional info
tomuben May 6, 2022
03dfc8d
Changes from code review
tomuben May 12, 2022
2870623
Updated dependency to script-languages-container-ci
tomuben May 12, 2022
9b07a22
Add trailing newline to buildspecs
tomuben May 12, 2022
83d3004
Add trailing newline to buildspecs
tomuben May 12, 2022
c02c1a2
Add trailing newline to buildspecs
tomuben May 12, 2022
3f78559
Updated documentation for release and test-release flows.
tomuben May 12, 2022
404955a
1. tiny changes in flow diagrams
tomuben May 13, 2022
832b589
Changed logic about how to pass GH Token to GithhubDraftReleaseCreator
tomuben May 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
.pytest_cache
.gitignore
1 change: 1 addition & 0 deletions doc/changes/changes_0.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ n/a
## Features / Enhancements

- #1: Implement codebuild deployment and buildspec generation
- #3: Added release code build

## Documentation

Expand Down
7 changes: 6 additions & 1 deletion exasol_script_languages_container_ci_setup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from exasol_script_languages_container_ci_setup.cli.commands import (
health,
generate_buildspec,
generate_release_buildspec,
deploy_source_credentials,
deploy_ci_build,
deploy_release_build,
validate_ci_build,
validate_source_credentials
validate_release_build,
validate_source_credentials,
start_release_build,
start_test_release_build
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import sys
from typing import Optional

import click

Expand All @@ -19,7 +20,7 @@
@click.option('--project-url', type=str, required=True,
help="""The URL of the project on Github.""")
def deploy_ci_build(
aws_profile: str,
aws_profile: Optional[str],
log_level: str,
project: str,
project_url: str):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
import sys
from typing import Optional

import click

from exasol_script_languages_container_ci_setup.cli.cli import cli
from exasol_script_languages_container_ci_setup.cli.common import add_options
from exasol_script_languages_container_ci_setup.cli.options.logging import logging_options, set_log_level
from exasol_script_languages_container_ci_setup.lib.aws_access import AwsAccess
from exasol_script_languages_container_ci_setup.cli.options.aws_options import aws_options
from exasol_script_languages_container_ci_setup.lib.release_build import run_deploy_release_build


@cli.command()
@add_options(aws_options)
@add_options(logging_options)
@click.option('--project', type=str, required=True,
help="""The project for which the stack will be created.""")
@click.option('--project-url', type=str, required=True,
help="""The URL of the project on Github.""")
def deploy_release_build(
aws_profile: Optional[str],
log_level: str,
project: str,
project_url: str):
set_log_level(log_level)
try:
run_deploy_release_build(AwsAccess(aws_profile), project, project_url)
except Exception:
logging.error("run_deploy_release_build failed.")
sys.exit(1)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import sys
from typing import Optional

import click

Expand All @@ -21,7 +22,7 @@
@click.option('--secret-token-key', required=True, type=str,
help="Token key stored as secret in AWS Secret Manager.")
def deploy_source_credentials(
aws_profile: str,
aws_profile: Optional[str],
log_level: str,
secret_name: str,
secret_user_key: str,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Tuple, Optional

import click

from exasol_script_languages_container_ci_setup.cli.cli import cli
from exasol_script_languages_container_ci_setup.cli.common import add_options
from exasol_script_languages_container_ci_setup.cli.options.logging import logging_options, set_log_level
from exasol_script_languages_container_ci_setup.lib.run_generate_release_buildspec import run_generate_release_buildspec


@cli.command()
@add_options(logging_options)
@click.option('--flavor-root-path', required=True, multiple=True,
type=click.Path(file_okay=False, dir_okay=True, exists=True),
help="Path where script language container flavors are located.")
@click.option('--output-path', type=click.Path(file_okay=False, dir_okay=True, exists=True, writable=True),
default="./aws-code-build/release", show_default=True,
help="Path where buildspec files will be deployed.")
@click.option('--config-file', type=click.Path(file_okay=True, dir_okay=False, exists=True),
help="Configuration file for build (project specific).")
def generate_release_buildspecs(
flavor_root_path: Tuple[str, ...],
log_level: str,
output_path: str,
config_file: Optional[str]
):
"""
This command generates the buildspec file(s) for the AWS CodeBuild Release build based
on the flavors located in path "flavor_root_path".
"""
set_log_level(log_level)
run_generate_release_buildspec(flavor_root_path, output_path, config_file)

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from typing import Optional

import click

from exasol_script_languages_container_ci_setup.cli.cli import cli
from exasol_script_languages_container_ci_setup.cli.common import add_options
from exasol_script_languages_container_ci_setup.cli.options.logging import logging_options, set_log_level
from exasol_script_languages_container_ci_setup.lib.aws_access import AwsAccess
from exasol_script_languages_container_ci_setup.cli.options.aws_options import aws_options
from exasol_script_languages_container_ci_setup.lib.run_start_release_build import run_start_release_build


@cli.command()
@add_options(aws_options)
@add_options(logging_options)
@click.option('--project', type=str, required=True,
help="""The project name. Must be same name as used for the AWS CodeBuild release stack creation.""")
@click.option('--upload-url', type=str, required=False,
help="""The URL of the Github release where artifacts will be stored.""")
@click.option('--branch', type=str, required=True,
help="""The branch of the repository which will be used.""")
def start_release_build(
aws_profile: Optional[str],
log_level: str,
project: str,
upload_url: str,
branch: str):
"""
This command triggers the AWS release Codebuild to upload the
release artifacts onto the given Github release, indicated by parameter 'upload_url'.
"""
set_log_level(log_level)
run_start_release_build(AwsAccess(aws_profile), project, upload_url, branch, os.getenv("GITHUB_TOKEN"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os
from typing import Optional

import click

from exasol_script_languages_container_ci_setup.cli.cli import cli
from exasol_script_languages_container_ci_setup.cli.common import add_options
from exasol_script_languages_container_ci_setup.cli.options.logging import logging_options, set_log_level
from exasol_script_languages_container_ci_setup.lib.aws_access import AwsAccess
from exasol_script_languages_container_ci_setup.cli.options.aws_options import aws_options
from exasol_script_languages_container_ci_setup.lib.github_draft_release_creator import GithubDraftReleaseCreator
from exasol_script_languages_container_ci_setup.lib.run_start_release_build import run_start_test_release_build


@cli.command()
@add_options(aws_options)
@add_options(logging_options)
@click.option('--project', type=str, required=True,
help="""The project name. Must be same name as used for the AWS CodeBuild release stack creation.""")
@click.option('--repo-name', type=str, required=True,
help="""The repository for which the test release should be created.
For example 'exasol/script-languages'.""", )
@click.option('--branch', type=str, required=True,
help="""The branch for which the test release should be created.""")
@click.option('--release-title', type=str, required=True,
help="""The title of the Github draft release which will be created.""")
def start_test_release_build(
aws_profile: Optional[str],
log_level: str,
repo_name: str,
project: str,
branch: str,
release_title: str
):
"""
This command creates a release draft on Github and triggers the AWS release Codebuild to upload the
release artifacts onto the new Github release.
"""
set_log_level(log_level)
run_start_test_release_build(AwsAccess(aws_profile), GithubDraftReleaseCreator(),
repo_name, project, branch, release_title, os.getenv("GITHUB_TOKEN"))
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Optional

import click

Expand All @@ -18,7 +19,7 @@
@click.option('--project-url', type=str, required=True,
help="""The URL of the project on Github.""")
def validate_ci_build(
aws_profile: str,
aws_profile: Optional[str],
log_level: str,
project: str,
project_url: str):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Optional

import click

from exasol_script_languages_container_ci_setup.cli.cli import cli
from exasol_script_languages_container_ci_setup.cli.common import add_options
from exasol_script_languages_container_ci_setup.cli.options.logging import logging_options, set_log_level
from exasol_script_languages_container_ci_setup.lib.aws_access import AwsAccess
from exasol_script_languages_container_ci_setup.cli.options.aws_options import aws_options
from exasol_script_languages_container_ci_setup.lib.release_build import run_validate_release_build


@cli.command()
@add_options(aws_options)
@add_options(logging_options)
@click.option('--project', type=str, required=True,
help="""The project for which the stack will be created.""")
@click.option('--project-url', type=str, required=True,
help="""The URL of the project on Github.""")
def validate_release_build(
aws_profile: Optional[str],
log_level: str,
project: str,
project_url: str):
set_log_level(log_level)
run_validate_release_build(AwsAccess(aws_profile), project, project_url)
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

import click

from exasol_script_languages_container_ci_setup.cli.cli import cli
Expand All @@ -18,7 +20,7 @@
@click.option('--secret-token-key', required=True, type=str,
help="Github user token key stored as secret in AWS Secret Manager under the respective secret name.")
def validate_source_credentials(
aws_profile: str,
aws_profile: Optional[str],
log_level: str,
secret_name: str,
secret_user_key: str,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import click

aws_options = [
click.option('--aws-profile', required=True, type=str,
click.option('--aws-profile', required=False, type=str,
help="Id of the AWS profile to use."),
]
101 changes: 82 additions & 19 deletions exasol_script_languages_container_ci_setup/lib/aws_access.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import Optional
import time
from typing import Optional, List, Dict, Any, Iterable

import boto3
from botocore.exceptions import ClientError
Expand All @@ -11,6 +12,13 @@ class AwsAccess(object):
def __init__(self, aws_profile: Optional[str]):
self._aws_profile = aws_profile

@property
def aws_profile_for_logging(self) -> str:
if self._aws_profile is not None:
return self._aws_profile
else:
return "{default}"

@property
def aws_profile(self) -> Optional[str]:
return self._aws_profile
Expand All @@ -19,13 +27,8 @@ def upload_cloudformation_stack(self, yml: str, stack_name: str):
"""
Deploy the cloudformation stack.
"""
if self._aws_profile is not None:
logging.debug(f"Running upload_cloudformation_stack for aws profile {self._aws_profile}")
aws_session = boto3.session.Session(profile_name=self._aws_profile)
cloud_client = aws_session.client('cloudformation')
else:
logging.debug(f"Running upload_cloudformation_stack for default aws profile.")
cloud_client = boto3.client('cloudformation')
logging.debug(f"Running upload_cloudformation_stack for aws profile {self.aws_profile_for_logging}")
cloud_client = self._get_aws_client("cloudformation")
try:
cfn_deployer = Deployer(cloudformation_client=cloud_client)
result = cfn_deployer.create_and_wait_for_changeset(stack_name=stack_name, cfn_template=yml,
Expand All @@ -48,9 +51,8 @@ def read_secret_arn(self, secret_name: str):
Uses Boto3 to retrieve the ARN of a secret.
"""
logging.debug(f"Reading secret for getting ARN, secret name = {secret_name}, "
f"for aws profile {self._aws_profile}")
session = boto3.session.Session(profile_name=self._aws_profile)
client = session.client(service_name='secretsmanager')
f"for aws profile {self.aws_profile_for_logging}")
client = self._get_aws_client(service_name='secretsmanager')

try:
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
Expand All @@ -69,12 +71,73 @@ def validate_cloudformation_template(self, cloudformation_yml) -> None:
Pitfall: Boto3 expects the YAML string as parameter, whereas the AWS CLI expects the file URL as parameter.
It requires to have the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env variables set correctly.
"""
if self._aws_profile is not None:
logging.debug(f"Running validate_cloudformation_template for aws profile {self._aws_profile}")
aws_session = boto3.session.Session(profile_name=self._aws_profile)
cloud_client = aws_session.client('cloudformation')
cloud_client.validate_template(TemplateBody=cloudformation_yml)
logging.debug(f"Running validate_cloudformation_template for aws profile {self.aws_profile_for_logging}")
cloud_client = self._get_aws_client("cloudformation")
cloud_client.validate_template(TemplateBody=cloudformation_yml)

def _get_aws_client(self, service_name: str) -> Any:
tomuben marked this conversation as resolved.
Show resolved Hide resolved
if self._aws_profile is None:
return boto3.client(service_name)
aws_session = boto3.session.Session(profile_name=self._aws_profile)
return aws_session.client(service_name)

def get_all_stack_resources(self, stack_name: str) -> List[Dict[str, str]]:
"""
This functions uses Boto3 to get all AWS Cloudformation resources for a specific Cloudformation stack,
identified by parameter `stack_name`.
The AWS API truncates at a size of 1MB, and in order to get all chunks the method must be called
passing the previous retrieved token until no token is returned.
"""
logging.debug(f"Running get_all_codebuild_projects for aws profile {self.aws_profile_for_logging}")
cf_client = self._get_aws_client('cloudformation')
current_result = cf_client.list_stack_resources(StackName=stack_name)
result = current_result["StackResourceSummaries"]

while "nextToken" in current_result:
current_result = cf_client.list_projects(StackName=stack_name, nextToken=current_result["nextToken"])
result.extend(current_result["StackResourceSummaries"])
return result

def start_codebuild(self, project: str, environment_variables_overrides: List[Dict[str, str]], branch: str) -> None:
"""
This functions uses Boto3 to start a batch build.
It forwards all variables from parameter env_variables as environment variables to the CodeBuild project.
If a branch is given, it starts the codebuild for the given branch.
After the build has triggered it waits until the batch build finished
:raises
`RuntimeError` if build fails or AWS Batch build returns unknown status
"""
codebuild_client = self._get_aws_client("codebuild")
logging.info(f"Trigger codebuild for project {project} with branch {branch} "
f"and env_variables ({environment_variables_overrides})")
ret_val = codebuild_client.start_build_batch(projectName=project,
sourceVersion=branch,
environmentVariablesOverride=list(
environment_variables_overrides))

def wait_for(seconds: int, interval: int) -> Iterable[int]:
for _ in range(int(seconds / interval)):
yield interval

build_id = ret_val['buildBatch']['id']
logging.debug(f"Codebuild for project {project} with branch {branch} triggered. Id is {build_id}.")
interval = 30
timeout_time_in_seconds = 60 * 60 * 2 # We wait for maximal 2h + (something)
for seconds_to_wait in wait_for(seconds=timeout_time_in_seconds, interval=interval):
time.sleep(seconds_to_wait)
logging.debug(f"Checking status of codebuild id {build_id}.")
build_response = codebuild_client.batch_get_build_batches(ids=[build_id])
logging.debug(f"Build response of codebuild id {build_id} is {build_response}")
if len(build_response['buildBatches']) != 1:
logging.error(f"Unexpected return value from 'batch_get_build_batches': {build_response}")
build_status = build_response['buildBatches'][0]['buildBatchStatus']
logging.info(f"Build status of codebuild id {build_id} is {build_status}")
if build_status == 'SUCCEEDED':
break
elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']:
raise RuntimeError(f"Build ({build_id}) failed with status: {build_status}")
elif build_status != "IN_PROGRESS":
raise RuntimeError(f"Batch build {build_id} has unknown build status: {build_status}")
# if loop does not break early, build wasn't successful
else:
logging.debug(f"Running validate_cloudformation_template for default aws profile.")
cloud_client = boto3.client('cloudformation')
cloud_client.validate_template(TemplateBody=cloudformation_yml)
raise RuntimeError(f"Batch build {build_id} ran into timeout.")
Loading