-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathact
executable file
·377 lines (319 loc) · 15.8 KB
/
act
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""ACT: Automated Containerization Tool
About:
This is the main entry for act.
USAGE:
$ act <build|push> [OPTIONS]
Example:
$ act build -h
$ act push -h
"""
from __future__ import print_function
from genericpath import isdir
from subprocess import CalledProcessError
from src.utils import (initialize,
fatal,
err,
require,
permissions,
exists)
import sys, os, subprocess
import argparse, textwrap
import warnings
__version__ = 'v1.0.0'
def bash(cmd, interpreter='/bin/bash', strict=True, cwd='/home/', **kwargs):
"""
Interface to run a process or bash command. Using subprocess.call_check()
due to portability across most python versions. It was introduced in python 2.5
and it is also interoperabie across all python 3 versions.
@param cmd <str>:
Shell command to run
@param interpreter <str>:
Interpreter for command to run [default: bash]
@pararm strict <bool>:
Prefixes any command with 'set -euo pipefail' to ensure process fail with
the expected exit-code
@params kwargs <check_call()>:
Keyword arguments to modify subprocess.check_call() behavior
@return exitcode <int>:
Returns the exit code of the run command, failures return non-zero exit codes
"""
# Changes behavior of default shell
prefix = ''
# set -e: exit immediately upon error
# set -u: treats unset variables as an error
# set -o pipefail: exits if a error occurs in any point of a pipeline
if strict: prefix = 'set -euo pipefail; '
exitcode = subprocess.check_call(prefix + cmd, shell=True, executable=interpreter, cwd=cwd, **kwargs)
if exitcode != 0:
fatal("""\n\tFatal: Failed to run '{}' command!
└── Command returned a non-zero exitcode of '{}'.
""".format(process, exitcode)
)
return exitcode
def build(sub_args):
"""Creates a docker image from GitHub repository
@param sub_args <parser.parse_args() object>:
Parsed arguments for run sub-command
"""
repo_url = sub_args.repo_url
img_name = sub_args.img_name.lower()
output = sub_args.output
skip_build = sub_args.skip_build
base_image = sub_args.base_img
use_dockta_reqs = sub_args.use_dockta_reqs
repo_name = repo_url.split('/')[-1].split('.git')[0]
repo_dir = os.path.join(output, repo_name)
repo_dir_lowercase = os.path.join(output, repo_name.lower())
print('Repository url: {}'.format(repo_url))
print('Image name: {}'.format(img_name))
print('Repo directory: {}'.format(repo_dir))
print('Starting build..')
if (not os.path.isdir(output)):
os.mkdir(output)
if (os.path.isdir(repo_dir_lowercase) or os.path.isdir(repo_dir)):
warnings.warn('Repo directory already exists, skipping clone..')
else:
bash('git -C {0} clone {1}'.format(output, repo_url))
# Workaround for lowercase repo name requirement
if (os.path.isdir(repo_dir) and not repo_dir.split('/')[-1].islower()):
# Only rename repo directory
# if it is not lower case
bash('mv {0} {1}'.format(repo_dir, repo_dir_lowercase))
# Do not use the users defined requirements
# Let Dockta try to determine the its requirements
if (use_dockta_reqs):
if exists(os.path.join(repo_dir_lowercase, 'requirements.txt')):
bash('rm {}'.format(os.path.join(repo_dir_lowercase, 'requirements.txt')))
if exists(os.path.join(repo_dir_lowercase, 'Dockerfile')):
bash('rm {}'.format(os.path.join(repo_dir_lowercase, 'Dockerfile')))
bash('dockta compile --from {0} {1}'.format(base_image,repo_dir_lowercase))
if (not skip_build):
# Build the Docker image
dockerfile = '.Dockerfile' # Default Dockerfile name generated by Dockta
if exists(os.path.join(repo_dir_lowercase, 'Dockerfile')):
# Dockerfile already exists in Github repo
# use that Dockerfile instead
dockerfile = 'Dockerfile'
if not exists(os.path.join(repo_dir_lowercase, 'requirements.txt')):
filelist = open(os.path.join(repo_dir_lowercase, dockerfile), 'r').readlines()
for line in filelist:
if line.startswith('RUN pip3 install --requirement requirements.txt'):
# Edit Dockerfile with builds for R packages
# AND a requirements.txt is not present
bash("sed -i 's/RUN pip3 install --requirement requirements.txt \\\$//g' {}".format(dockerfile),
cwd=repo_dir_lowercase)
bash("sed -i 's@^ && bash -c \"Rscript@RUN bash -c \"Rscript@' {}".format(dockerfile),
cwd=repo_dir_lowercase)
break
# Check for compatiability with bionic cran mirror
if base_image == 'ubuntu:18.04':
# Edit Dockerfile for compatiability with Ubuntu Bionic
bash("sed -i 's@eoan-cran@bionic-cran@' {}".format(dockerfile), cwd=repo_dir_lowercase)
bash('docker build --no-cache -f {} --tag={} .'.format(dockerfile, img_name),
cwd=repo_dir_lowercase)
return
def push(sub_args):
"""Pushes a local docker image to a Docker Registry.
@param sub_args <parser.parse_args() object>:
Parsed arguments for run sub-command
"""
print(sub_args)
img_name = sub_args.img_name
registry = sub_args.registry
tag = sub_args.tag
if (tag is None):
tag = 'latest'
bash('docker tag {1}:latest {0}/{1}:{2}'.format(registry,img_name,tag))
try:
bash('docker push {0}/{1}:{2}'.format(registry,img_name,tag))
except CalledProcessError as e:
print(e)
print('Please try running "$ docker login" first before pushing')
return
def parsed_arguments():
"""Parses user-provided command-line arguments. Requires argparse and textwrap
package. argparse was added to standard lib in python 3.2 and textwrap was added
in python 3.5. To create custom help formatting for subparsers a docstring is
used create the help message for required options. argparse does not support named
subparser groups, which is normally what would be used to accomphish this reformatting.
As so, the help message for require options must be suppressed. If a new required arg
is added to a subparser, it must be added to the docstring and the usage statement
also must be updated.
"""
# Create a top-level parser
parser = argparse.ArgumentParser(description = 'ACT: \
Automated GitHub Containerization Tool')
# Adding Verison information
parser.add_argument('--version', action = 'version', version='%(prog)s {}'.format(__version__))
# Create sub-command parser
subparsers = parser.add_subparsers(help='List of available sub-commands')
# Options for the "build" sub-command
# Grouped sub-parser arguments are currently not supported.
# https://bugs.python.org/issue9341
# Here is a work around to create more useful help message for named
# options that are required! Please note: if a required arg is added the
# description below should be updated (i.e. update usage and add new option)
required_build_options = textwrap.dedent("""\
usage: act build [-h] [--skip-build]
[--base-img BASE_IMG]
[--use-dockta-reqs]
--repo-url REPO_URL
--img-name IMG_NAME
--output OUTPUT
Creates a Dockerfile for a Github repository and builds the
local Docker image. Please see the push sub command to push
a local image to a Docker Registry. The build sub command
takes a Github URL and a output directory to clone the Github
repository.
required arguments:
--repo-url REPO_URL URL of the Github repository to build
an Docker image.
--img-name IMG_NAME Name of the local docker image to be
built. This image name can be provided
to the push sub command to push the
image to a Docker Registry.
--output OUTPUT Path to an output directory. This
path is where ACT will clone the
Github repository and create the
Dockerfile. If the provided output
directory does not exist, it will
be created automatically.
After the build sub command completes,
a Dockerfile will be created in the
user provided output directory and a
local Docker image will be built.
""")
# Display example usage in epilog
build_epilog = textwrap.dedent("""\
example:
# Step 1.) Create Dockerfile and build the docker image.
act build --repo-url https://github.com/CCBR/AAsap \\
--img-name ccbr_aasap \\
--output /home/$USER/scratch/
version:
{}
""".format(__version__))
# Supressing help message of required args to overcome no sub-parser named groups
subparser_build = subparsers.add_parser('build',
help = 'Creates Dockerfile and builds Docker image.',
usage = argparse.SUPPRESS,
formatter_class=argparse.RawDescriptionHelpFormatter,
description = required_build_options,
epilog = build_epilog)
# Input Github URL for build sub command
subparser_build.add_argument('--repo-url',
type = str,
required = True,
help = argparse.SUPPRESS)
# Name of locally built Docker Image
subparser_build.add_argument('--img-name',
type = str,
required = True,
help = argparse.SUPPRESS)
# Output Directory (build working directory)
subparser_build.add_argument('--output',
type = lambda option: os.path.abspath(os.path.expanduser(option)),
required = True,
help = argparse.SUPPRESS)
# Skip build option (only create Dockerfile)
subparser_build.add_argument('--skip-build',
action = 'store_true',
required = False,
default = False,
help = 'Only create a Dockerfile, skips docker build.')
# set dockta base image ... defaults to ubuntu:18.04
subparser_build.add_argument('--base-img',
type = str,
required = False,
default = "ubuntu:18.04",
help = 'Base image to use in the Dockerfile, \
default=ubuntu:18.04.')
subparser_build.add_argument('--use-dockta-reqs',
action = 'store_true',
required = False,
default = False,
help = 'Use dockta generated ".requirements.txt" file.')
# Options for the "push" sub-command
# Grouped sub-parser arguments are currently not supported by argparse.
# https://bugs.python.org/issue9341
# Here is a work around to create more useful help message for named
# options that are required! Please note: if a required arg is added the
# description below should be updated (i.e. update usage and add new option)
required_push_options = textwrap.dedent("""\
usage: act push [-h] [--tag TAG]
--img-name IMG_NAME
--registry REGISTRY
Pushes a local docker image to a Docker registry. Please
see the build sub command to for information on how to build
a local Docker image. The push sub command takes the name of
a local image, a Docker registry prefix, and a optional tag
to tag and push an image to a registry like Dockerhub.
required arguments:
--img-name IMG_NAME Name of the local docker image to be
pushed to a Docker registry. This image
name was defined via the '--img-name'
option of the build sub command.
Please see the build sub command
for more information.
--registry REGISTRY Docker registry prefix. If pushing
an image to a user account on DockerHub,
this will be the user's DockerHub username.
If push to a org account, this will be the
name of the Dockerhub organization.
""")
# Display example usage in epilog
push_epilog = textwrap.dedent("""\
example:
# Step 1.) Push local docker image to DockerHub.
act push --img-name ccbr_aasap \\
--registry nciccbr \\
--tag v1.0.0
version:
{}
""".format(__version__))
# Supressing help message of required args to overcome no sub-parser named groups
subparser_push = subparsers.add_parser('push',
help = 'Push local Docker image to a Registry',
usage = argparse.SUPPRESS,
formatter_class=argparse.RawDescriptionHelpFormatter,
description = required_push_options,
epilog = push_epilog)
# Name of locally built Docker Image
subparser_push.add_argument('--img-name',
type = str,
required = True,
help = argparse.SUPPRESS)
# Docker registry to prefix (for DockerHub: either user name or org name)
subparser_push.add_argument('--registry',
type = str,
required = True,
help = argparse.SUPPRESS)
# Tag for Docker image
subparser_push.add_argument('--tag',
required = False,
default = '',
help = 'Tag for Docker image, defaults to latest.')
# Sanity check for user command line arguments
if len(sys.argv) < 2:
parser.error("""\n\t └── Fatal: failed to provide a valid sub command to act!
Please run 'act -h' to view more information about act's usage.""".format(
sys.argv[0])
)
# Define handlers for each sub-parser
subparser_build.set_defaults(func = build)
subparser_push.set_defaults(func = push)
# Parse command-line args
args = parser.parse_args()
return args
def main():
# Display version information
if '--version' not in sys.argv:
print('Automated Containerization Tool (ACT {})'.format(__version__))
# Collect args for sub-command
args = parsed_arguments()
# Mediator method to call sub-command's set handler function
args.func(args)
if __name__ == '__main__':
main()