Skip to content

Commit 732dbe3

Browse files
ElasticDL Client: Implement "zoo init | build | push" command. (#2082)
* Initial version for the parsers. * Add unit test for argument parser. * Add Api module for the command call of ElasticDL client. * Initial implementation for zoo init and zoo build. * BASE_IMAGE is not replaced by the variable. * Update comments * Add cluster_spec in Dockerfile. * Add docker_base_url in command line arguments. * Add args module to build the command line arguments. * Add more description about the command arguments. * Add more argument description. * Do some re-format * Add the process to copy cluster spec file to the current directory. * Remove placeholder test cases.
1 parent 5a32263 commit 732dbe3

File tree

8 files changed

+323
-3
lines changed

8 files changed

+323
-3
lines changed

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[settings]
22
multi_line_output=3
33
line_length=79
4-
known_third_party = PIL,deepctr,docker,google,grpc,kubernetes,numpy,odps,pandas,recordio,requests,setuptools,sklearn,tensorflow,yaml
4+
known_third_party = PIL,deepctr,docker,google,grpc,jinja2,kubernetes,numpy,odps,pandas,recordio,requests,setuptools,sklearn,tensorflow,yaml
55
include_trailing_comma=True

elasticdl_client/api.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Copyright 2020 The ElasticDL Authors. All rights reserved.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
import os
15+
import shutil
16+
17+
import docker
18+
from jinja2 import Template
19+
20+
21+
def init_zoo(args):
22+
print("Create the Dockerfile for the model zoo.")
23+
24+
# Copy cluster spec file to the current directory if specified
25+
cluster_spec_path = args.cluster_spec
26+
cluster_spec_name = None
27+
if cluster_spec_path:
28+
if not os.path.exists(cluster_spec_path):
29+
raise RuntimeError(
30+
"The cluster spec {} doesn't exist".format(cluster_spec_path)
31+
)
32+
shutil.copy2(cluster_spec_path, os.getcwd())
33+
cluster_spec_name = os.path.basename(cluster_spec_path)
34+
35+
# Create the docker file
36+
# Build the content from the template and arguments
37+
tmpl_str = """\
38+
FROM {{ BASE_IMAGE }} as base
39+
40+
RUN pip install elasticdl_preprocessing
41+
RUN pip install elasticdl
42+
43+
COPY . /model_zoo
44+
{% if EXTRA_PYPI_INDEX %}
45+
RUN pip install -r /model_zoo/requirements.txt\
46+
--extra-index-url={{ EXTRA_PYPI_INDEX }}\
47+
{% else %}\
48+
RUN pip install -r /model_zoo/requirements.txt\
49+
{% endif %}
50+
51+
{% if CLUSTER_SPEC_NAME %}\
52+
COPY ./{{ CLUSTER_SPEC_NAME }} /cluster_spec/{{ CLUSTER_SPEC_NAME }}\
53+
{% endif %}
54+
"""
55+
template = Template(tmpl_str)
56+
docker_file_content = template.render(
57+
BASE_IMAGE=args.base_image,
58+
EXTRA_PYPI_INDEX=args.extra_pypi_index,
59+
CLUSTER_SPEC_NAME=cluster_spec_name,
60+
)
61+
62+
with open("./Dockerfile", mode="w+") as f:
63+
f.write(docker_file_content)
64+
65+
66+
def build_zoo(args):
67+
print("Build the image for the model zoo.")
68+
# Call docker api to build the image
69+
# Validate the image name schema
70+
client = _get_docker_client(
71+
docker_base_url=args.docker_base_url,
72+
docker_tlscert=args.docker_tlscert,
73+
docker_tlskey=args.docker_tlskey,
74+
)
75+
for line in client.build(
76+
dockerfile="./Dockerfile",
77+
path=args.path,
78+
rm=True,
79+
tag=args.image,
80+
decode=True,
81+
):
82+
_print_docker_progress(line)
83+
84+
85+
def push_zoo(args):
86+
print("Push the image for the model zoo.")
87+
# Call docker api to push the image to remote registry
88+
client = _get_docker_client(
89+
docker_base_url=args.docker_base_url,
90+
docker_tlscert=args.docker_tlscert,
91+
docker_tlskey=args.docker_tlskey,
92+
)
93+
94+
for line in client.push(args.image, stream=True, decode=True):
95+
_print_docker_progress(line)
96+
97+
98+
def _get_docker_client(docker_base_url, docker_tlscert, docker_tlskey):
99+
if docker_tlscert and docker_tlskey:
100+
tls_config = docker.tls.TLSConfig(
101+
client_cert=(docker_tlscert, docker_tlskey)
102+
)
103+
return docker.APIClient(base_url=docker_base_url, tls=tls_config)
104+
else:
105+
return docker.APIClient(base_url=docker_base_url)
106+
107+
108+
def _print_docker_progress(line):
109+
error = line.get("error", None)
110+
if error:
111+
raise RuntimeError("Docker image build: " + error)
112+
stream = line.get("stream", None)
113+
if stream:
114+
print(stream, end="")
115+
else:
116+
print(line)

elasticdl_client/common/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright 2020 The ElasticDL Authors. All rights reserved.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.

elasticdl_client/common/args.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2020 The ElasticDL Authors. All rights reserved.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
15+
def add_zoo_init_arguments(parser):
16+
parser.add_argument(
17+
"--base_image",
18+
type=str,
19+
default="python:latest",
20+
help="Base Docker image.",
21+
)
22+
parser.add_argument(
23+
"--extra_pypi_index",
24+
type=str,
25+
help="The extra URLs of Python package repository indexes",
26+
required=False,
27+
)
28+
parser.add_argument(
29+
"--cluster_spec",
30+
type=str,
31+
help="The file that contains user-defined cluster specification,"
32+
"the file path can be accessed by ElasticDL client.",
33+
default="",
34+
)
35+
36+
37+
def add_zoo_build_arguments(parser):
38+
parser.add_argument(
39+
"path", type=str, help="The path where the build context locate."
40+
)
41+
parser.add_argument(
42+
"--image",
43+
type=str,
44+
required=True,
45+
help="The name of the docker image we are building for"
46+
"this model zoo.",
47+
)
48+
add_docker_arguments(parser)
49+
50+
51+
def add_zoo_push_arguments(parser):
52+
parser.add_argument(
53+
"image",
54+
type=str,
55+
help="The name of the docker image for this model zoo.",
56+
)
57+
add_docker_arguments(parser)
58+
59+
60+
def add_docker_arguments(parser):
61+
parser.add_argument(
62+
"--docker_base_url",
63+
type=str,
64+
help="URL to the Docker server",
65+
default="unix://var/run/docker.sock",
66+
)
67+
parser.add_argument(
68+
"--docker_tlscert",
69+
type=str,
70+
help="Path to Docker client cert",
71+
default="",
72+
)
73+
parser.add_argument(
74+
"--docker_tlskey",
75+
type=str,
76+
help="Path to Docker client key",
77+
default="",
78+
)

elasticdl_client/main.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,44 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313

14+
import argparse
15+
16+
from elasticdl_client.api import build_zoo, init_zoo, push_zoo
17+
from elasticdl_client.common import args
18+
19+
20+
def build_argument_parser():
21+
parser = argparse.ArgumentParser()
22+
subparsers = parser.add_subparsers()
23+
subparsers.required = True
24+
25+
# Initialize the parser for the `elasticdl zoo` commands
26+
zoo_parser = subparsers.add_parser("zoo")
27+
zoo_subparsers = zoo_parser.add_subparsers()
28+
zoo_subparsers.required = True
29+
30+
# elasticdl zoo init
31+
zoo_init_parser = zoo_subparsers.add_parser("init")
32+
zoo_init_parser.set_defaults(func=init_zoo)
33+
args.add_zoo_init_arguments(zoo_init_parser)
34+
35+
# elasticdl zoo build
36+
zoo_build_parser = zoo_subparsers.add_parser("build")
37+
zoo_build_parser.set_defaults(func=build_zoo)
38+
args.add_zoo_build_arguments(zoo_build_parser)
39+
40+
# elasticdl zoo push
41+
zoo_push_parser = zoo_subparsers.add_parser("push")
42+
zoo_push_parser.set_defaults(func=push_zoo)
43+
args.add_zoo_push_arguments(zoo_push_parser)
44+
45+
return parser
46+
1447

1548
def main():
16-
pass
49+
parser = build_argument_parser()
50+
args, _ = parser.parse_known_args()
51+
args.func(args)
1752

1853

1954
if __name__ == "__main__":

elasticdl_client/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
kubernetes==10.1.0
2-
docker
2+
docker==4.2.1
3+
Jinja2==2.11.2
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2020 The ElasticDL Authors. All rights reserved.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
import unittest
15+
16+
from elasticdl_client.main import build_argument_parser
17+
18+
19+
class ArgParserTest(unittest.TestCase):
20+
def setUp(self):
21+
self._parser = build_argument_parser()
22+
23+
def test_parse_zoo_init(self):
24+
args = ["zoo", "init"]
25+
args = self._parser.parse_args(args)
26+
self.assertEqual(args.base_image, "python:latest")
27+
args.func(args)
28+
29+
args = ["zoo", "init", "--base_image=elasticdl:base"]
30+
args = self._parser.parse_args(args)
31+
self.assertEqual(args.base_image, "elasticdl:base")
32+
33+
with self.assertRaises(SystemExit):
34+
args = ["zoo", "init", "--mock_param=mock_value"]
35+
args = self._parser.parse_args(args)
36+
37+
def test_parse_zoo_build(self):
38+
args = [
39+
"zoo",
40+
"build",
41+
"--image=a_docker_registry/bright/elasticdl-wnd:1.0",
42+
".",
43+
]
44+
args = self._parser.parse_args(args)
45+
self.assertEqual(
46+
args.image, "a_docker_registry/bright/elasticdl-wnd:1.0"
47+
)
48+
self.assertEqual(args.path, ".")
49+
50+
with self.assertRaises(SystemExit):
51+
args = ["zoo", "build", "."]
52+
args = self._parser.parse_args(args)
53+
54+
with self.assertRaises(SystemExit):
55+
args = [
56+
"zoo",
57+
"build",
58+
"--image=a_docker_registry/bright/elasticdl-wnd:1.0",
59+
]
60+
args = self._parser.parse_args(args)
61+
62+
def test_parse_zoo_push(self):
63+
args = ["zoo", "push", "a_docker_registry/bright/elasticdl-wnd:1.0"]
64+
args = self._parser.parse_args(args)
65+
self.assertEqual(
66+
args.image, "a_docker_registry/bright/elasticdl-wnd:1.0"
67+
)
68+
69+
with self.assertRaises(SystemExit):
70+
args = ["zoo", "push"]
71+
args = self._parser.parse_args(args)

setup_client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,11 @@
2626
python_requires=">=3.5",
2727
packages=find_packages(include=["elasticdl_client*"]),
2828
package_data={"": ["requirements.txt"]},
29+
# TODO(brightcoder01): Use `elasticdl_client` as the binary name
30+
# temporarily. After we move all the functinality from main package
31+
# to elasticdl_client package, we will rename the binary name to
32+
# `elasticdl` and then remove the entry_points in main package.
33+
entry_points={
34+
"console_scripts": ["elasticdl_client=elasticdl_client.main:main"]
35+
},
2936
)

0 commit comments

Comments
 (0)