Skip to content

Commit e3426c9

Browse files
authored
Merge pull request #27 from unity-sds/push_ecr
Add ability to push into an AWS ECR registry using environment credentials
2 parents 56c83e0 + eed8749 commit e3426c9

File tree

6 files changed

+150
-27
lines changed

6 files changed

+150
-27
lines changed

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
app_pack_generator>=1.0.0
1+
boto3
2+
app_pack_generator>=1.1.0
23
unity-sds-client>=0.2.0

unity_app_generator/__main__.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,26 @@ def init(args):
5454
def build_docker(args):
5555
state_dir = check_state_directory(state_directory_path(args))
5656

57-
app_gen = UnityApplicationGenerator(state_dir, use_owner=args.use_owner)
57+
app_gen = UnityApplicationGenerator(state_dir,
58+
use_namespace=args.image_namespace,
59+
use_repository=args.image_repository,
60+
use_tag=args.image_tag)
5861

5962
app_gen.create_docker_image()
6063

6164
def push_docker(args):
6265
state_dir = check_state_directory(state_directory_path(args))
6366

6467
app_gen = UnityApplicationGenerator(state_dir)
65-
66-
app_gen.push_to_docker_registry(args.container_registry, image_tag=args.image_tag)
68+
69+
app_gen.push_to_docker_registry(args.container_registry)
70+
71+
def push_ecr(args):
72+
state_dir = check_state_directory(state_directory_path(args))
73+
74+
app_gen = UnityApplicationGenerator(state_dir)
75+
76+
app_gen.push_to_aws_ecr()
6777

6878
def notebook_parameters(args):
6979

@@ -94,6 +104,9 @@ def main():
94104
parser.add_argument("--state_directory",
95105
help=f"An alternative location to store the application state other than {STATE_DIRECTORY}")
96106

107+
parser.add_argument("--verbose", "-v", action="store_true", default=False,
108+
help=f"Enable verbose logging")
109+
97110
# init
98111
subparsers = parser.add_subparsers(required=True)
99112

@@ -116,8 +129,14 @@ def main():
116129
parser_build_docker = subparsers.add_parser('build_docker',
117130
help=f"Build a Docker image from the initialized application directory")
118131

119-
parser_build_docker.add_argument("--no_owner", dest="use_owner", action="store_false", default=True,
120-
help="Disable using the owner of the Git repository in the Docker image tag")
132+
parser_build_docker.add_argument("-n", "--image_namespace",
133+
help="Docker image namespace to use instead of the automatically generated one from the Git repository owner. An empty string removes the namespace from the image reference.")
134+
135+
parser_build_docker.add_argument("-r", "--image_repository",
136+
help="Docker image repository to use instead of the automatically generated one from the Git repository name.")
137+
138+
parser_build_docker.add_argument("-t", "--image_tag",
139+
help="Docker image tag to use instead of the automatically generated one from the Git commit id")
121140

122141
parser_build_docker.set_defaults(func=build_docker)
123142

@@ -129,11 +148,15 @@ def main():
129148
parser_push_docker.add_argument("container_registry",
130149
help="URL or Dockerhub username of a Docker registry for pushing of the built image")
131150

132-
parser_push_docker.add_argument("-t", "--image_tag",
133-
help="Docker image tag to push into container registry if already built without using the build_docker subcommand")
134-
135151
parser_push_docker.set_defaults(func=push_docker)
136152

153+
# push_ecr
154+
155+
parser_push_ecr = subparsers.add_parser('push_ecr',
156+
help=f"Push a Docker image from the initialized application directory to an AWS Elastic Container Registry (ECR)")
157+
158+
parser_push_ecr.set_defaults(func=push_ecr)
159+
137160
# notebook_parameters
138161

139162
parser_parameters = subparsers.add_parser('parameters',
@@ -174,7 +197,10 @@ def main():
174197

175198
args = parser.parse_args()
176199

177-
logging.basicConfig(level=logging.DEBUG)
200+
if args.verbose:
201+
logging.basicConfig(level=logging.DEBUG)
202+
else:
203+
logging.basicConfig(level=logging.INFO)
178204

179205
try:
180206
args.func(args)
@@ -185,4 +211,4 @@ def main():
185211
#app_gen.push_to_application_registry(None)
186212

187213
if __name__ == '__main__':
188-
main()
214+
main()

unity_app_generator/ecr_helper.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import base64
2+
3+
import boto3
4+
from botocore.exceptions import ClientError
5+
6+
import logging
7+
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from app_pack_generator import DockerUtil
12+
13+
logger = logging.getLogger(__name__)
14+
15+
class ECRHelper(object):
16+
17+
def __init__(self, docker_util : 'DockerUtil'):
18+
19+
self.docker_util = docker_util
20+
self.ecr_client = boto3.client("ecr")
21+
22+
def create_repository(self):
23+
24+
if self.docker_util.image_namespace is not None and self.docker_util.image_namespace != "":
25+
aws_repo_name = f"{self.docker_util.image_namespace}/{self.docker_util.image_repository}"
26+
else:
27+
aws_repo_name = self.docker_util.image_repository
28+
29+
logger.info(f"Creating AWS ECR repository named: {aws_repo_name}")
30+
31+
try:
32+
response = self.ecr_client.create_repository(
33+
repositoryName=aws_repo_name,
34+
)
35+
36+
return response["repository"]['repositoryUri']
37+
38+
except ClientError as err:
39+
if err.response["Error"]["Code"] == "RepositoryAlreadyExistsException":
40+
logger.debug(f"Repository {aws_repo_name} already exists.")
41+
response = self.ecr_client.describe_repositories(
42+
repositoryNames=[aws_repo_name]
43+
)
44+
45+
return response['repositories'][0]['repositoryUri']
46+
else:
47+
logger.error(
48+
"Error creating repository %s. Here's why %s",
49+
repository_name,
50+
err.response["Error"]["Message"],
51+
)
52+
raise
53+
54+
def docker_login(self):
55+
56+
logger.info("Logging into Docker using ECR credentials")
57+
58+
token = self.ecr_client.get_authorization_token()
59+
username, password = base64.b64decode(token['authorizationData'][0]['authorizationToken']).decode().split(':')
60+
61+
# Remove the protocol prefix for this to work
62+
# https://github.com/docker/docker-py/issues/2256
63+
registry = token['authorizationData'][0]['proxyEndpoint'].replace("https://", "")
64+
65+
response = self.docker_util.docker_client.login(
66+
username=username,
67+
password=password,
68+
registry=registry
69+
)
70+
71+
return registry

unity_app_generator/generator.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from glob import glob
66

77
from .state import ApplicationState
8+
from .ecr_helper import ECRHelper
89

910
from app_pack_generator import GitManager, DockerUtil, ApplicationNotebook
1011
from app_pack_generator import ProcessCWL, DataStagingCWL, Descriptor
@@ -18,7 +19,8 @@ class ApplicationGenerationError(Exception):
1819

1920
class UnityApplicationGenerator(object):
2021

21-
def __init__(self, state_directory, source_repository=None, destination_directory=None, checkout=None, use_owner=True):
22+
def __init__(self, state_directory, source_repository=None, destination_directory=None, checkout=None,
23+
use_namespace=None, use_repository=None, use_tag=None):
2224

2325
if not ApplicationState.exists(state_directory):
2426
self.repo_info = self._localize_source(source_repository, destination_directory, checkout)
@@ -27,34 +29,54 @@ def __init__(self, state_directory, source_repository=None, destination_director
2729
self.app_state = ApplicationState(state_directory)
2830
self.repo_info = self._localize_source(self.app_state.source_repository, self.app_state.app_base_path, checkout)
2931

30-
self.docker_util = DockerUtil(self.repo_info, do_prune=False, use_owner=use_owner)
32+
# Use value from command line to override values saved into app state
33+
image_namespace = use_namespace if use_namespace is not None else self.app_state.docker_image_namespace
34+
image_repository = use_repository if use_repository is not None else self.app_state.docker_image_repository
35+
image_tag = use_tag if use_tag is not None else self.app_state.docker_image_tag
36+
37+
self.docker_util = DockerUtil(self.repo_info, do_prune=False,
38+
use_namespace=image_namespace,
39+
use_repository=image_repository,
40+
use_tag=image_tag)
3141

3242
def _localize_source(self, source, dest, checkout):
3343

3444
# Check out original repository
3545
git_mgr = GitManager(source, dest)
3646

3747
if checkout is not None:
38-
logger.debug(f"Checking out {checkout} in {repo_dir}")
48+
logger.info(f"Checking out {checkout} in {repo_dir}")
3949
git_mgr.checkout(checkout)
4050

4151
return git_mgr
4252

4353
def create_docker_image(self):
54+
55+
# These come either from the commandline or are generated by Docker util
56+
self.app_state.docker_image_namespace = self.docker_util.image_namespace
57+
self.app_state.docker_image_repository = self.docker_util.image_repository
58+
self.app_state.docker_image_tag = self.docker_util.image_tag
4459

4560
# Create Docker image
46-
self.app_state.docker_image_tag = self.docker_util.repo2docker()
47-
48-
def push_to_docker_registry(self, docker_registry, image_tag=None):
61+
self.app_state.docker_image_reference = self.docker_util.build_image()
4962

50-
if image_tag is not None:
51-
self.app_state.docker_image_tag = image_tag
52-
53-
if self.app_state.docker_image_tag is None:
54-
raise ApplicationGenerationError("Cannot push Docker image to registry without a valid tag. Run the Docker build command or supply an image tag as an argument.")
63+
def push_to_docker_registry(self, docker_registry):
5564

5665
# Push to remote repository
57-
self.app_state.docker_url = self.docker_util.push_image(docker_registry, self.app_state.docker_image_tag)
66+
self.app_state.docker_url = self.docker_util.push_image(docker_registry, self.app_state.docker_image_reference)
67+
68+
def push_to_aws_ecr(self):
69+
70+
ecr_helper = ECRHelper(self.docker_util)
71+
72+
# Create an ECR registry if it doesn't already exist
73+
ecr_helper.create_repository()
74+
75+
# Log in to ECR via Docker
76+
registry_url = ecr_helper.docker_login()
77+
78+
# Push docker image into ECR
79+
self.push_to_docker_registry(registry_url)
5880

5981
def _generate_dockstore_cwl(self, cwl_output_path, target_cwl_filename):
6082

@@ -77,12 +99,12 @@ def _generate_dockstore_cwl(self, cwl_output_path, target_cwl_filename):
7799

78100
def create_cwl(self, cwl_output_path=None, docker_url=None, monolithic=False):
79101

80-
# Fall through using docker_image_tag if docker_url does not exist because no push has occurred
102+
# Fall through using docker_image_reference if docker_url does not exist because no push has occurred
81103
# Or if docker_url is supplied as an argument use that
82104
if docker_url is None and self.app_state.docker_url is not None:
83105
docker_url = self.app_state.docker_url
84-
elif docker_url is None and self.app_state.docker_image_tag is not None:
85-
docker_url = self.app_state.docker_image_tag
106+
elif docker_url is None and self.app_state.docker_image_reference is not None:
107+
docker_url = self.app_state.docker_image_reference
86108
elif docker_url is None:
87109
raise ApplicationGenerationError("Cannot create CWL files when Docker image tag or URL has not yet been registered through building and/or pushing Docker image")
88110

unity_app_generator/state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ class ApplicationState(object):
1717
"cwl_output_path": "",
1818

1919
"source_repository": None,
20+
"docker_image_namespace": None,
21+
"docker_image_repository": None,
2022
"docker_image_tag": None,
23+
"docker_image_reference": None,
2124
"docker_url": None,
2225
"app_registry_id": None,
2326
}

unity_app_generator/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__="1.0.0"
1+
__version__="1.1.0"

0 commit comments

Comments
 (0)