Martin Blanchard pushed to branch mablanch/160-docker-compose at BuildGrid / buildgrid
Commits:
-
78efe8c6
by Santiago Gil at 2019-02-18T16:32:52Z
-
d998a99d
by Martin Blanchard at 2019-02-18T16:45:41Z
-
4ad8bfc7
by Martin Blanchard at 2019-02-18T16:45:41Z
-
b6a04805
by Martin Blanchard at 2019-02-18T16:45:41Z
-
a4095c45
by Martin Blanchard at 2019-02-18T16:45:41Z
-
fbae1c15
by Martin Blanchard at 2019-02-18T16:45:41Z
-
83fb50b9
by Martin Blanchard at 2019-02-18T16:45:41Z
-
ddd3258b
by Martin Blanchard at 2019-02-18T16:45:41Z
24 changed files:
- + .dockerignore
- .gitlab-ci.yml
- − BUILDSTREAM_README.rst
- Dockerfile
- buildgrid/_app/commands/cmd_server.py
- − buildgrid/_app/settings/default.yml
- buildgrid/_app/settings/reference.yml
- − buildgrid/_app/settings/remote-storage.yml
- buildgrid/server/bots/instance.py
- buildgrid/server/instance.py
- buildgrid/server/scheduler.py
- buildgrid/_app/settings/cas.yml → data/config/artifacts.conf
- + data/config/controller.conf
- + data/config/default.conf
- + data/config/storage.conf
- + docker-compose.yml
- docs/source/installation.rst
- docs/source/using_cas_server.rst
- docs/source/using_internal.rst
- + requirements.auth.txt
- + requirements.docs.txt
- + requirements.tests.txt
- + requirements.txt
- setup.py
Changes:
1 |
+# Ignore everything:
|
|
2 |
+*
|
|
3 |
+ |
|
4 |
+# Whitelist:
|
|
5 |
+!buildgrid
|
|
6 |
+!data/config
|
|
7 |
+!tests
|
|
8 |
+!LICENSE
|
|
9 |
+!README.rst
|
|
10 |
+!requirements.txt
|
|
11 |
+!requirements.auth.txt
|
|
12 |
+!setup.cfg
|
|
13 |
+!setup.py
|
|
14 |
+!_version.py
|
|
15 |
+!.coveragerc
|
|
16 |
+!.pylintrc
|
... | ... | @@ -31,7 +31,7 @@ before_script: |
31 | 31 |
.run-dummy-job-template: &dummy-job
|
32 | 32 |
stage: test
|
33 | 33 |
script:
|
34 |
- - ${BGD} server start buildgrid/_app/settings/default.yml &
|
|
34 |
+ - ${BGD} server start data/config/default.conf &
|
|
35 | 35 |
- sleep 1 # Allow server to boot
|
36 | 36 |
- ${BGD} bot dummy &
|
37 | 37 |
- ${BGD} cas upload-dummy
|
1 |
-Temp Demo Instructions
|
|
2 |
-======================
|
|
3 |
- |
|
4 |
-A quick guide to getting remote execution working with BuildStream. Please change URL and certifcates / keys to your own.
|
|
5 |
- |
|
6 |
-Downloaded and build::
|
|
7 |
- |
|
8 |
- https://gitlab.com/BuildStream/buildbox
|
|
9 |
- |
|
10 |
-Copy build to bin/.
|
|
11 |
- |
|
12 |
-Checkout branch::
|
|
13 |
- |
|
14 |
- https://gitlab.com/BuildStream/buildstream/tree/jmac/source_pushing_experiments
|
|
15 |
- |
|
16 |
-Update to your URL::
|
|
17 |
- |
|
18 |
- https://gitlab.com/BuildStream/buildstream/blob/jmac/source_pushing_experiments/buildstream/sandbox/_sandboxremote.py#L73
|
|
19 |
- |
|
20 |
-Start artifact server::
|
|
21 |
- |
|
22 |
- bst-artifact-server --port 11001 --server-key server.key --server-cert server.crt --client-certs client.crt --enable-push /home/user/
|
|
23 |
- |
|
24 |
-Start bgd server::
|
|
25 |
- |
|
26 |
- bgd server start
|
|
27 |
- |
|
28 |
-Run::
|
|
29 |
- |
|
30 |
- bgd bot buildbox
|
|
31 |
- |
|
32 |
-Update project.conf in build area with::
|
|
33 |
- |
|
34 |
- artifacts:
|
|
35 |
- url: https://localhost:11001
|
|
36 |
- server-cert: server.crt
|
|
37 |
- |
|
38 |
- # Optional client key pair for authentication
|
|
39 |
- client-key: client.key
|
|
40 |
- client-cert: client.crt
|
|
41 |
- |
|
42 |
- push: true
|
|
43 |
- |
|
44 |
-Run build with::
|
|
45 |
- |
|
46 |
- bst build --track something.bst
|
1 |
-FROM python:3.5-stretch
|
|
1 |
+##
|
|
2 |
+# BuildGrid's Docker build manifest.
|
|
3 |
+#
|
|
4 |
+# ¡FOR LOCAL DEVELOPMENT ONLY!
|
|
5 |
+#
|
|
6 |
+# Builds an image from local sources.
|
|
7 |
+#
|
|
2 | 8 |
|
3 |
-# Point the path to where buildgrid gets installed
|
|
4 |
-ENV PATH=$PATH:/root/.local/bin/
|
|
9 |
+FROM python:3.5-slim-stretch
|
|
5 | 10 |
|
6 |
-# Upgrade python modules
|
|
7 |
-RUN python3 -m pip install --upgrade setuptools pip
|
|
8 |
- |
|
9 |
-# Use /app as the current working directory
|
|
11 |
+# Use /app as working directory:
|
|
10 | 12 |
WORKDIR /app
|
11 | 13 |
|
12 |
-# Copy the repo contents (source, config files, etc) in the WORKDIR
|
|
13 |
-COPY . .
|
|
14 |
+# Create a virtual environment:
|
|
15 |
+RUN [ \
|
|
16 |
+"python3", "-m", "venv", "/app/env" \
|
|
17 |
+]
|
|
18 |
+ |
|
19 |
+# Upgrade Python core modules:
|
|
20 |
+RUN [ \
|
|
21 |
+"/app/env/bin/python", "-m", "pip", \
|
|
22 |
+"install", "--upgrade", \
|
|
23 |
+"setuptools", "pip", "wheel" \
|
|
24 |
+]
|
|
25 |
+ |
|
26 |
+# Install the main requirements:
|
|
27 |
+ADD requirements.txt /app
|
|
28 |
+RUN [ \
|
|
29 |
+"/app/env/bin/python", "-m", "pip", \
|
|
30 |
+"install", "--requirement", \
|
|
31 |
+"requirements.txt" \
|
|
32 |
+]
|
|
33 |
+ |
|
34 |
+# Install the auth. requirements:
|
|
35 |
+ADD requirements.auth.txt /app
|
|
36 |
+RUN [ \
|
|
37 |
+"/app/env/bin/python", "-m", "pip", \
|
|
38 |
+"install", "--requirement", \
|
|
39 |
+"requirements.auth.txt" \
|
|
40 |
+]
|
|
41 |
+ |
|
42 |
+# Copy the repo. contents:
|
|
43 |
+COPY . /app
|
|
14 | 44 |
|
15 |
-# Install BuildGrid
|
|
16 |
-RUN pip install --user --editable .
|
|
45 |
+# Install BuildGrid:
|
|
46 |
+RUN [ \
|
|
47 |
+"/app/env/bin/python", "-m", "pip", \
|
|
48 |
+"install", "--editable", \
|
|
49 |
+".[auth,tests]" \
|
|
50 |
+]
|
|
17 | 51 |
|
18 |
-# Entry Point of the image (should get an additional argument from CMD, the path to the config file)
|
|
19 |
-ENTRYPOINT ["bgd", "server", "start", "-vv"]
|
|
52 |
+# Entry-point for the image:
|
|
53 |
+ENTRYPOINT [ \
|
|
54 |
+"/app/env/bin/bgd" \
|
|
55 |
+]
|
|
20 | 56 |
|
21 |
-# Default config file (used if no CMD specified when running)
|
|
22 |
-CMD ["buildgrid/_app/settings/default.yml"]
|
|
57 |
+# Default command (default config.):
|
|
58 |
+CMD [ \
|
|
59 |
+"server", "start", \
|
|
60 |
+"data/config/default.conf", \
|
|
61 |
+"-vvv" \
|
|
62 |
+]
|
... | ... | @@ -119,6 +119,14 @@ def _create_server_from_config(configuration): |
119 | 119 |
click.echo("Error: Configuration, {}.".format(e), err=True)
|
120 | 120 |
sys.exit(-1)
|
121 | 121 |
|
122 |
+ if 'thread-pool-size' in configuration:
|
|
123 |
+ try:
|
|
124 |
+ kargs['max_workers'] = int(configuration['thread-pool-size'])
|
|
125 |
+ |
|
126 |
+ except ValueError as e:
|
|
127 |
+ click.echo("Error: Configuration, {}.".format(e), err=True)
|
|
128 |
+ sys.exit(-1)
|
|
129 |
+ |
|
122 | 130 |
server = BuildGridServer(**kargs)
|
123 | 131 |
|
124 | 132 |
try:
|
1 |
-server:
|
|
2 |
- - !channel
|
|
3 |
- port: 50051
|
|
4 |
- insecure-mode: true
|
|
5 |
- |
|
6 |
-description: |
|
|
7 |
- A single default instance.
|
|
8 |
- |
|
9 |
-instances:
|
|
10 |
- - name: ''
|
|
11 |
- description: |
|
|
12 |
- The main server
|
|
13 |
- |
|
14 |
- storages:
|
|
15 |
- - !disk-storage &main-storage
|
|
16 |
- path: !expand-path $HOME/cas
|
|
17 |
- |
|
18 |
- services:
|
|
19 |
- - !action-cache &main-action
|
|
20 |
- storage: *main-storage
|
|
21 |
- max-cached-refs: 256
|
|
22 |
- allow-updates: true
|
|
23 |
- |
|
24 |
- - !execution
|
|
25 |
- storage: *main-storage
|
|
26 |
- action-cache: *main-action
|
|
27 |
- |
|
28 |
- - !cas
|
|
29 |
- storage: *main-storage
|
|
30 |
- |
|
31 |
- - !bytestream
|
|
32 |
- storage: *main-storage
|
... | ... | @@ -136,3 +136,7 @@ monitoring: |
136 | 136 |
# binary - Protobuf binary format.
|
137 | 137 |
# json - JSON format.
|
138 | 138 |
serialization-format: binary
|
139 |
+ |
|
140 |
+##
|
|
141 |
+# Maximum number of gRPC threads.
|
|
142 |
+thread-pool-size: 20
|
1 |
-server:
|
|
2 |
- - !channel
|
|
3 |
- port: 50051
|
|
4 |
- insecure-mode: true
|
|
5 |
- |
|
6 |
-description: |
|
|
7 |
- A single default instance with remote storage.
|
|
8 |
- |
|
9 |
-instances:
|
|
10 |
- - name: ''
|
|
11 |
- description: |
|
|
12 |
- The main server
|
|
13 |
- |
|
14 |
- storages:
|
|
15 |
- - !remote-storage &main-storage
|
|
16 |
- url: http://localhost:50052
|
|
17 |
- instance-name: main
|
|
18 |
- # credentials:
|
|
19 |
- # tls-client-key: null
|
|
20 |
- # tls-client-cert: null
|
|
21 |
- # tls-server-cert: null
|
|
22 |
- |
|
23 |
- services:
|
|
24 |
- - !action-cache &main-action
|
|
25 |
- storage: *main-storage
|
|
26 |
- max-cached-refs: 256
|
|
27 |
- allow-updates: true
|
|
28 |
- |
|
29 |
- - !execution
|
|
30 |
- storage: *main-storage
|
|
31 |
- action-cache: *main-action
|
|
32 |
- |
|
33 |
- - !cas
|
|
34 |
- storage: *main-storage
|
|
35 |
- |
|
36 |
- - !bytestream
|
|
37 |
- storage: *main-storage
|
... | ... | @@ -32,6 +32,8 @@ class BotsInterface: |
32 | 32 |
|
33 | 33 |
def __init__(self, scheduler):
|
34 | 34 |
self.__logger = logging.getLogger(__name__)
|
35 |
+ # Turn on debug mode based on log verbosity level:
|
|
36 |
+ self.__debug = self.__logger.getEffectiveLevel() <= logging.DEBUG
|
|
35 | 37 |
|
36 | 38 |
self._scheduler = scheduler
|
37 | 39 |
self._instance_name = None
|
... | ... | @@ -65,13 +67,11 @@ class BotsInterface: |
65 | 67 |
register with the service, the old one should be closed along
|
66 | 68 |
with all its jobs.
|
67 | 69 |
"""
|
68 |
- bot_id = bot_session.bot_id
|
|
69 |
- |
|
70 |
- if bot_id == "":
|
|
71 |
- raise InvalidArgumentError("bot_id needs to be set by client")
|
|
70 |
+ if not bot_session.bot_id:
|
|
71 |
+ raise InvalidArgumentError("Bot's id must be set by client.")
|
|
72 | 72 |
|
73 | 73 |
try:
|
74 |
- self._check_bot_ids(bot_id)
|
|
74 |
+ self._check_bot_ids(bot_session.bot_id)
|
|
75 | 75 |
except InvalidArgumentError:
|
76 | 76 |
pass
|
77 | 77 |
|
... | ... | @@ -79,21 +79,27 @@ class BotsInterface: |
79 | 79 |
name = "{}/{}".format(parent, str(uuid.uuid4()))
|
80 | 80 |
bot_session.name = name
|
81 | 81 |
|
82 |
- self._bot_ids[name] = bot_id
|
|
83 |
- |
|
84 |
- self.__logger.info("Created bot session name=[%s] with bot_id=[%s]", name, bot_id)
|
|
82 |
+ self._bot_ids[name] = bot_session.bot_id
|
|
85 | 83 |
|
86 | 84 |
# We want to keep a copy of lease ids we have assigned
|
87 | 85 |
self._assigned_leases[name] = set()
|
88 | 86 |
|
89 | 87 |
self._request_leases(bot_session)
|
88 |
+ |
|
89 |
+ if self.__debug:
|
|
90 |
+ self.__logger.info("Opened session name=[%s] for bot=[%s], leases=[%s]",
|
|
91 |
+ bot_session.name, bot_session.bot_id,
|
|
92 |
+ ",".join([lease.id[:8] for lease in bot_session.leases]))
|
|
93 |
+ else:
|
|
94 |
+ self.__logger.info("Opened session, name=[%s] for bot=[%s]",
|
|
95 |
+ bot_session.name, bot_session.bot_id)
|
|
96 |
+ |
|
90 | 97 |
return bot_session
|
91 | 98 |
|
92 | 99 |
def update_bot_session(self, name, bot_session):
|
93 | 100 |
""" Client updates the server. Any changes in state to the Lease should be
|
94 | 101 |
registered server side. Assigns available leases with work.
|
95 | 102 |
"""
|
96 |
- self.__logger.debug("Updating bot session name=[%s]", name)
|
|
97 | 103 |
self._check_bot_ids(bot_session.bot_id, name)
|
98 | 104 |
self._check_assigned_leases(bot_session)
|
99 | 105 |
|
... | ... | @@ -111,6 +117,15 @@ class BotsInterface: |
111 | 117 |
bot_session.leases.remove(lease)
|
112 | 118 |
|
113 | 119 |
self._request_leases(bot_session)
|
120 |
+ |
|
121 |
+ if self.__debug:
|
|
122 |
+ self.__logger.info("Sending session update, name=[%s], for bot=[%s], leases=[%s]",
|
|
123 |
+ bot_session.name, bot_session.bot_id,
|
|
124 |
+ ",".join([lease.id[:8] for lease in bot_session.leases]))
|
|
125 |
+ else:
|
|
126 |
+ self.__logger.info("Sending session update, name=[%s], for bot=[%s]",
|
|
127 |
+ bot_session.name, bot_session.bot_id)
|
|
128 |
+ |
|
114 | 129 |
return bot_session
|
115 | 130 |
|
116 | 131 |
# --- Private API ---
|
... | ... | @@ -91,6 +91,8 @@ class BuildGridServer: |
91 | 91 |
self.__grpc_server = grpc.server(self.__grpc_executor,
|
92 | 92 |
options=(('grpc.so_reuseport', 0),))
|
93 | 93 |
|
94 |
+ self.__logger.debug("Setting up gRPC server with thread-limit=[%s]", max_workers)
|
|
95 |
+ |
|
94 | 96 |
self.__main_loop = asyncio.get_event_loop()
|
95 | 97 |
|
96 | 98 |
self.__monitoring_bus = None
|
... | ... | @@ -22,6 +22,7 @@ Schedules jobs. |
22 | 22 |
import bisect
|
23 | 23 |
from datetime import timedelta
|
24 | 24 |
import logging
|
25 |
+from threading import Lock
|
|
25 | 26 |
|
26 | 27 |
from buildgrid._enums import LeaseState, OperationStage
|
27 | 28 |
from buildgrid._exceptions import NotFoundError
|
... | ... | @@ -53,6 +54,7 @@ class Scheduler: |
53 | 54 |
self.__jobs_by_name = {} # Name to Job 1:1 mapping
|
54 | 55 |
|
55 | 56 |
self.__queue = []
|
57 |
+ self.__queue_lock = Lock()
|
|
56 | 58 |
|
57 | 59 |
self._is_instrumented = monitor
|
58 | 60 |
|
... | ... | @@ -297,27 +299,26 @@ class Scheduler: |
297 | 299 |
worker properties, configuration and state at the time of the
|
298 | 300 |
request.
|
299 | 301 |
"""
|
300 |
- if not self.__queue:
|
|
301 |
- return []
|
|
302 |
- |
|
303 |
- # Looking for the first job that could be assigned to the worker...
|
|
304 |
- for job_index, job in enumerate(self.__queue):
|
|
305 |
- if self._worker_is_capable(worker_capabilities, job):
|
|
306 |
- self.__logger.info("Job scheduled to run: [%s]", job.name)
|
|
302 |
+ # TODO: Replace with a more efficient way of doing this.
|
|
303 |
+ with self.__queue_lock:
|
|
304 |
+ # Looking for the first job that could be assigned to the worker...
|
|
305 |
+ for job_index, job in enumerate(self.__queue):
|
|
306 |
+ if self._worker_is_capable(worker_capabilities, job):
|
|
307 |
+ self.__logger.info("Job scheduled to run: [%s]", job.name)
|
|
307 | 308 |
|
308 |
- lease = job.lease
|
|
309 |
+ lease = job.lease
|
|
309 | 310 |
|
310 |
- if not lease:
|
|
311 |
- # For now, one lease at a time:
|
|
312 |
- lease = job.create_lease()
|
|
311 |
+ if not lease:
|
|
312 |
+ # For now, one lease at a time:
|
|
313 |
+ lease = job.create_lease()
|
|
313 | 314 |
|
314 |
- if lease:
|
|
315 |
- del self.__queue[job_index]
|
|
316 |
- return [lease]
|
|
315 |
+ if lease:
|
|
316 |
+ del self.__queue[job_index]
|
|
317 |
+ return [lease]
|
|
317 | 318 |
|
318 |
- return None
|
|
319 |
+ return []
|
|
319 | 320 |
|
320 |
- return None
|
|
321 |
+ return []
|
|
321 | 322 |
|
322 | 323 |
def update_job_lease_state(self, job_name, lease):
|
323 | 324 |
"""Requests a state transition for a job's current :class:Lease.
|
... | ... | @@ -551,11 +552,12 @@ class Scheduler: |
551 | 552 |
"""Schedules or reschedules a job."""
|
552 | 553 |
job = self.__jobs_by_name[job_name]
|
553 | 554 |
|
554 |
- if job.operation_stage == OperationStage.QUEUED:
|
|
555 |
- self.__queue.sort()
|
|
555 |
+ with self.__queue_lock:
|
|
556 |
+ if job.operation_stage == OperationStage.QUEUED:
|
|
557 |
+ self.__queue.sort()
|
|
556 | 558 |
|
557 |
- else:
|
|
558 |
- bisect.insort(self.__queue, job)
|
|
559 |
+ else:
|
|
560 |
+ bisect.insort(self.__queue, job)
|
|
559 | 561 |
|
560 | 562 |
self.__logger.info("Job queued: [%s]", job.name)
|
561 | 563 |
|
... | ... | @@ -564,7 +566,8 @@ class Scheduler: |
564 | 566 |
job = self.__jobs_by_name[job_name]
|
565 | 567 |
|
566 | 568 |
if job.operation_stage == OperationStage.QUEUED:
|
567 |
- self.__queue.remove(job)
|
|
569 |
+ with self.__queue_lock:
|
|
570 |
+ self.__queue.remove(job)
|
|
568 | 571 |
|
569 | 572 |
del self.__jobs_by_action[job.action_digest.hash]
|
570 | 573 |
del self.__jobs_by_name[job.name]
|
... | ... | @@ -2,31 +2,34 @@ server: |
2 | 2 |
- !channel
|
3 | 3 |
port: 50052
|
4 | 4 |
insecure-mode: true
|
5 |
- # credentials:
|
|
6 |
- # tls-server-key: null
|
|
7 |
- # tls-server-cert: null
|
|
8 |
- # tls-client-certs: null
|
|
9 | 5 |
|
10 |
-description: |
|
|
11 |
- Just a CAS with some reference storage.
|
|
6 |
+description: >
|
|
7 |
+ Artifact server configuration:
|
|
8 |
+ - Unauthenticated plain HTTP at :50052
|
|
9 |
+ - Single instance: (empty-name)
|
|
10 |
+ - On-disk data stored in $HOME
|
|
11 |
+ - Hosted services:
|
|
12 |
+ - ReferenceStorage
|
|
13 |
+ - ContentAddressableStorage
|
|
14 |
+ - ByteStream
|
|
12 | 15 |
|
13 | 16 |
instances:
|
14 | 17 |
- name: ''
|
15 | 18 |
description: |
|
16 |
- The main server
|
|
19 |
+ The unique '' instance.
|
|
17 | 20 |
|
18 | 21 |
storages:
|
19 |
- - !disk-storage &main-storage
|
|
20 |
- path: !expand-path $HOME/cas
|
|
22 |
+ - !disk-storage &data-store
|
|
23 |
+ path: !expand-path $HOME/.cache/buildgrid/store
|
|
21 | 24 |
|
22 | 25 |
services:
|
23 | 26 |
- !cas
|
24 |
- storage: *main-storage
|
|
27 |
+ storage: *data-store
|
|
25 | 28 |
|
26 | 29 |
- !bytestream
|
27 |
- storage: *main-storage
|
|
30 |
+ storage: *data-store
|
|
28 | 31 |
|
29 | 32 |
- !reference-cache
|
30 |
- storage: *main-storage
|
|
31 |
- max-cached-refs: 256
|
|
33 |
+ storage: *data-store
|
|
34 |
+ max-cached-refs: 512
|
|
32 | 35 |
allow-updates: true
|
1 |
+server:
|
|
2 |
+ - !channel
|
|
3 |
+ port: 50051
|
|
4 |
+ insecure-mode: true
|
|
5 |
+ |
|
6 |
+description: >
|
|
7 |
+ Docker Compose controller configuration:
|
|
8 |
+ - Unauthenticated plain HTTP at :50051
|
|
9 |
+ - Single instance: local
|
|
10 |
+ - Expects a remote CAS at :50052
|
|
11 |
+ - Hosted services:
|
|
12 |
+ - ActionCache
|
|
13 |
+ - Execute
|
|
14 |
+ |
|
15 |
+authorization:
|
|
16 |
+ method: none
|
|
17 |
+ |
|
18 |
+monitoring:
|
|
19 |
+ enabled: false
|
|
20 |
+ |
|
21 |
+instances:
|
|
22 |
+ - name: local
|
|
23 |
+ description: |
|
|
24 |
+ The unique 'local' instance.
|
|
25 |
+ |
|
26 |
+ storages:
|
|
27 |
+ - !remote-storage &data-store
|
|
28 |
+ url: http://storage:50052
|
|
29 |
+ instance-name: local
|
|
30 |
+ |
|
31 |
+ services:
|
|
32 |
+ - !action-cache &build-cache
|
|
33 |
+ storage: *data-store
|
|
34 |
+ max-cached-refs: 256
|
|
35 |
+ cache-failed-actions: true
|
|
36 |
+ allow-updates: true
|
|
37 |
+ |
|
38 |
+ - !execution
|
|
39 |
+ storage: *data-store
|
|
40 |
+ action-cache: *build-cache
|
|
41 |
+ |
|
42 |
+thread-pool-size: 200
|
1 |
+server:
|
|
2 |
+ - !channel
|
|
3 |
+ port: 50051
|
|
4 |
+ insecure-mode: true
|
|
5 |
+ |
|
6 |
+description: >
|
|
7 |
+ BuildGrid's default configuration:
|
|
8 |
+ - Unauthenticated plain HTTP at :50052
|
|
9 |
+ - Single instance: main
|
|
10 |
+ - In-memory data, max. 2Gio
|
|
11 |
+ - Hosted services:
|
|
12 |
+ - ActionCache
|
|
13 |
+ - Execute
|
|
14 |
+ - ContentAddressableStorage
|
|
15 |
+ - ByteStream
|
|
16 |
+ |
|
17 |
+authorization:
|
|
18 |
+ method: none
|
|
19 |
+ |
|
20 |
+monitoring:
|
|
21 |
+ enabled: false
|
|
22 |
+ |
|
23 |
+instances:
|
|
24 |
+ - name: ''
|
|
25 |
+ description: |
|
|
26 |
+ The unique '' instance.
|
|
27 |
+ |
|
28 |
+ storages:
|
|
29 |
+ - !lru-storage &data-store
|
|
30 |
+ size: 2048M
|
|
31 |
+ |
|
32 |
+ services:
|
|
33 |
+ - !action-cache &build-cache
|
|
34 |
+ storage: *data-store
|
|
35 |
+ max-cached-refs: 256
|
|
36 |
+ cache-failed-actions: true
|
|
37 |
+ allow-updates: true
|
|
38 |
+ |
|
39 |
+ - !execution
|
|
40 |
+ storage: *data-store
|
|
41 |
+ action-cache: *build-cache
|
|
42 |
+ |
|
43 |
+ - !cas
|
|
44 |
+ storage: *data-store
|
|
45 |
+ |
|
46 |
+ - !bytestream
|
|
47 |
+ storage: *data-store
|
1 |
+server:
|
|
2 |
+ - !channel
|
|
3 |
+ port: 50052
|
|
4 |
+ insecure-mode: true
|
|
5 |
+ |
|
6 |
+description: >
|
|
7 |
+ Docker Compose storage configuration:
|
|
8 |
+ - Unauthenticated plain HTTP at :50052
|
|
9 |
+ - Single instance: local
|
|
10 |
+ - On-disk data stored in /var
|
|
11 |
+ - Hosted services:
|
|
12 |
+ - ReferenceStorage
|
|
13 |
+ - ContentAddressableStorage
|
|
14 |
+ - ByteStream
|
|
15 |
+ |
|
16 |
+authorization:
|
|
17 |
+ method: none
|
|
18 |
+ |
|
19 |
+monitoring:
|
|
20 |
+ enabled: false
|
|
21 |
+ |
|
22 |
+instances:
|
|
23 |
+ - name: local
|
|
24 |
+ description: |
|
|
25 |
+ The unique 'local' instance.
|
|
26 |
+ |
|
27 |
+ storages:
|
|
28 |
+ - !disk-storage &data-store
|
|
29 |
+ path: /var/lib/buildgrid/store
|
|
30 |
+ |
|
31 |
+ services:
|
|
32 |
+ - !cas
|
|
33 |
+ storage: *data-store
|
|
34 |
+ |
|
35 |
+ - !bytestream
|
|
36 |
+ storage: *data-store
|
|
37 |
+ |
|
38 |
+ - !reference-cache
|
|
39 |
+ storage: *data-store
|
|
40 |
+ max-cached-refs: 1024
|
|
41 |
+ allow-updates: true
|
1 |
+##
|
|
2 |
+# BuildGrid's Docker Compose manifest.
|
|
3 |
+#
|
|
4 |
+# ¡FOR LOCAL DEVELOPMENT ONLY!
|
|
5 |
+#
|
|
6 |
+# Spins-up a 'local' grid instance:
|
|
7 |
+# - Controller at http://localhost:50051
|
|
8 |
+# - CAS + AC at: http://localhost:50052
|
|
9 |
+#
|
|
10 |
+# Basic usage:
|
|
11 |
+# - docker-compose build
|
|
12 |
+# - docker-compose up --scale bots=10
|
|
13 |
+# - docker-compose down
|
|
14 |
+# - docker volume inspect buildgrid_data
|
|
15 |
+# - docker volume rm buildgrid_data
|
|
16 |
+# - docker image rm buildgrid:local
|
|
17 |
+#
|
|
18 |
+version: "3.2"
|
|
19 |
+ |
|
20 |
+services:
|
|
21 |
+ storage:
|
|
22 |
+ build:
|
|
23 |
+ context: .
|
|
24 |
+ image: buildgrid:local
|
|
25 |
+ command: [
|
|
26 |
+ "server", "start", "-vvv",
|
|
27 |
+ "/app/config/storage.conf"]
|
|
28 |
+ volumes:
|
|
29 |
+ - type: volume
|
|
30 |
+ source: data
|
|
31 |
+ target: /var/lib/buildgrid/store
|
|
32 |
+ volume:
|
|
33 |
+ nocopy: true
|
|
34 |
+ - type: bind
|
|
35 |
+ source: ./data/config/storage.conf
|
|
36 |
+ target: /app/config/storage.conf
|
|
37 |
+ ports:
|
|
38 |
+ - "50052:50052"
|
|
39 |
+ networks:
|
|
40 |
+ - backend
|
|
41 |
+ - host
|
|
42 |
+ |
|
43 |
+ controller:
|
|
44 |
+ image: buildgrid:local
|
|
45 |
+ command: [
|
|
46 |
+ "server", "start", "-vvv",
|
|
47 |
+ "/app/config/controller.conf"]
|
|
48 |
+ volumes:
|
|
49 |
+ - type: bind
|
|
50 |
+ source: ./data/config/controller.conf
|
|
51 |
+ target: /app/config/controller.conf
|
|
52 |
+ ports:
|
|
53 |
+ - "50051:50051"
|
|
54 |
+ networks:
|
|
55 |
+ - backend
|
|
56 |
+ - host
|
|
57 |
+ |
|
58 |
+ bots: # To be scaled horizontaly
|
|
59 |
+ image: buildgrid:local
|
|
60 |
+ command: [
|
|
61 |
+ "bot", "--parent=local",
|
|
62 |
+ "--remote=http://controller:50051",
|
|
63 |
+ "--remote-cas=http://storage:50052",
|
|
64 |
+ "host-tools"]
|
|
65 |
+ depends_on:
|
|
66 |
+ - controller
|
|
67 |
+ networks:
|
|
68 |
+ - backend
|
|
69 |
+ |
|
70 |
+networks:
|
|
71 |
+ backend:
|
|
72 |
+ host:
|
|
73 |
+ |
|
74 |
+volumes:
|
|
75 |
+ data:
|
... | ... | @@ -21,20 +21,24 @@ How to install BuildGrid directly onto your machine. |
21 | 21 |
Prerequisites
|
22 | 22 |
~~~~~~~~~~~~~
|
23 | 23 |
|
24 |
-BuildGrid only supports ``python3 >= 3.5`` but has no system requirements. Main
|
|
25 |
-Python dependencies, automatically handled during installation, include:
|
|
24 |
+BuildGrid only supports ``python3 >= 3.5.3`` but has no system requirements.
|
|
25 |
+Main Python dependencies, automatically handled during installation, include:
|
|
26 | 26 |
|
27 | 27 |
- `boto3`_: the Amazon Web Services (AWS) SDK for Python.
|
28 | 28 |
- `click`_: a Python composable command line library.
|
29 | 29 |
- `grpcio`_: Google's `gRPC`_ Python interface.
|
30 |
+- `janus`_: a mixed sync-async Python queue.
|
|
30 | 31 |
- `protobuf`_: Google's `protocol-buffers`_ Python interface.
|
32 |
+- `PyYAML`_: a YAML parser and emitter for Python.
|
|
31 | 33 |
|
32 | 34 |
.. _boto3: https://pypi.org/project/boto3
|
33 | 35 |
.. _click: https://pypi.org/project/click
|
34 | 36 |
.. _grpcio: https://pypi.org/project/grpcio
|
35 | 37 |
.. _gRPC: https://grpc.io
|
38 |
+.. _janus: https://pypi.org/project/janus
|
|
36 | 39 |
.. _protobuf: https://pypi.org/project/protobuf
|
37 | 40 |
.. _protocol-buffers: https://developers.google.com/protocol-buffers
|
41 |
+.. _PyYAML: https://pypi.org/project/PyYAML
|
|
38 | 42 |
|
39 | 43 |
|
40 | 44 |
.. _install-host-source-install:
|
... | ... | @@ -42,20 +46,30 @@ Python dependencies, automatically handled during installation, include: |
42 | 46 |
Install from sources
|
43 | 47 |
~~~~~~~~~~~~~~~~~~~~
|
44 | 48 |
|
45 |
-BuildGrid has ``setuptools`` support. In order to install it to your home
|
|
46 |
-directory, typically under ``~/.local``, simply run:
|
|
49 |
+BuildGrid has ``setuptools`` support. We recommend installing it in a dedicated
|
|
50 |
+`virtual environment`_. In order to do so in an environment named ``env``
|
|
51 |
+placed in the source tree, run:
|
|
47 | 52 |
|
48 | 53 |
.. code-block:: sh
|
49 | 54 |
|
50 |
- git clone https://gitlab.com/BuildGrid/buildgrid.git && cd buildgrid
|
|
51 |
- pip3 install --user --editable .
|
|
55 |
+ git clone https://gitlab.com/BuildGrid/buildgrid.git
|
|
56 |
+ cd buildgrid
|
|
57 |
+ python3 -m venv env
|
|
58 |
+ env/bin/python -m pip install --upgrade setuptools pip wheel
|
|
59 |
+ env/bin/python -m pip install --editable .
|
|
52 | 60 |
|
53 |
-Additionally, and if your distribution does not already include it, you may
|
|
54 |
-have to adjust your ``PATH``, in ``~/.bashrc``, with:
|
|
61 |
+.. hint::
|
|
62 |
+ |
|
63 |
+ Once created, the virtual environment can be *activated* by sourcing the
|
|
64 |
+ ``env/bin/activate`` script. In an activated terminal session, simply run
|
|
65 |
+ ``deactivate`` to later *deactivate* it.
|
|
66 |
+ |
|
67 |
+Once completed, you can check that installation succeed by locally starting the
|
|
68 |
+BuildGrid server with default configuration. Simply run:
|
|
55 | 69 |
|
56 | 70 |
.. code-block:: sh
|
57 | 71 |
|
58 |
- export PATH="${PATH}:${HOME}/.local/bin"
|
|
72 |
+ env/bin/bgd server start data/config/default.conf -vvv
|
|
59 | 73 |
|
60 | 74 |
.. note::
|
61 | 75 |
|
... | ... | @@ -63,66 +77,111 @@ have to adjust your ``PATH``, in ``~/.bashrc``, with: |
63 | 77 |
``tests``. They declare required dependency for, respectively, authentication
|
64 | 78 |
and authorization management, generating documentation and running
|
65 | 79 |
unit-tests. They can be use as helpers for setting up a development
|
66 |
- environment. To use them simply run:
|
|
80 |
+ environment. To use them run:
|
|
67 | 81 |
|
68 | 82 |
.. code-block:: sh
|
69 | 83 |
|
70 |
- pip3 install --user --editable ".[auth,docs,tests]"
|
|
84 |
+ env/bin/python -m pip install --editable ".[auth,docs,tests]"
|
|
85 |
+ |
|
86 |
+.. _virtual environment: https://docs.python.org/3/library/venv.html
|
|
71 | 87 |
|
72 | 88 |
|
73 | 89 |
.. install-docker:
|
74 | 90 |
|
75 |
-Installation through Docker
|
|
76 |
----------------------------
|
|
91 |
+Install through Docker
|
|
92 |
+----------------------
|
|
77 | 93 |
|
78 |
-How to build a Docker image that runs BuildGrid.
|
|
94 |
+BuildGrid comes with Docker support for local development use-cases.
|
|
79 | 95 |
|
80 |
-.. _install-docker-prerequisites:
|
|
96 |
+.. caution::
|
|
81 | 97 |
|
82 |
-Prerequisites
|
|
83 |
-~~~~~~~~~~~~~
|
|
98 |
+ The Docker manifests are intended to be use for **local development only**.
|
|
99 |
+ Do **not** use them in production.
|
|
84 | 100 |
|
85 |
-A working Docker installation. Please consult `Docker's Getting Started Guide`_ if you don't already have it installed.
|
|
101 |
+Please consult the `Get Started with Docker`_ guide if you are looking for
|
|
102 |
+instructions on how to setup Docker on your machine.
|
|
86 | 103 |
|
87 |
-.. _`Docker's Getting Started Guide`: https://www.docker.com/get-started
|
|
104 |
+.. _`Get Started with Docker`: https://www.docker.com/get-started
|
|
88 | 105 |
|
89 | 106 |
|
90 | 107 |
.. _install-docker-build:
|
91 | 108 |
|
92 |
-Docker Container from Sources
|
|
93 |
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
109 |
+Docker build
|
|
110 |
+~~~~~~~~~~~~
|
|
94 | 111 |
|
95 |
-To clone the source code and build a Docker image, simply run:
|
|
112 |
+BuildGrid ships a ``Dockerfile`` manifest for building images from source using
|
|
113 |
+``docker build``. In order to produce a ``buildgrid:local`` base image, run:
|
|
96 | 114 |
|
97 | 115 |
.. code-block:: sh
|
98 | 116 |
|
99 |
- git clone https://gitlab.com/BuildGrid/buildgrid.git && cd buildgrid
|
|
100 |
- docker build -t buildgrid_server .
|
|
117 |
+ git clone https://gitlab.com/BuildGrid/buildgrid.git
|
|
118 |
+ cd buildgrid
|
|
119 |
+ docker build --tag buildgrid:local .
|
|
101 | 120 |
|
102 | 121 |
.. note::
|
103 | 122 |
|
104 |
- The image built will contain the contents of the source code directory, including
|
|
105 |
- configuration files.
|
|
106 |
-
|
|
123 |
+ The image built will contain the Python sources, including example
|
|
124 |
+ configuration files. The main endpoint is the ``bgd`` CLI tools and the
|
|
125 |
+ default command shall run the BuildGrid server loading default configuration.
|
|
126 |
+ |
|
127 |
+Once completed, you can check that build succeed by locally starting in a
|
|
128 |
+container the BuildGrid server with default configuration. Simply run:
|
|
129 |
+ |
|
130 |
+.. code-block:: sh
|
|
131 |
+ |
|
132 |
+ docker run --interactive --publish 50051:50051 buildgrid:local
|
|
133 |
+ |
|
107 | 134 |
.. hint::
|
108 | 135 |
|
109 |
- Whenever the source code is updated or new configuration files are made, you need to re-build
|
|
110 |
- the image.
|
|
136 |
+ You can run any of the BuildGrid CLI tool using that image, simply pass extra
|
|
137 |
+ arguments to ``docker run`` the same way you would pass them to ``bgd``.
|
|
138 |
+ |
|
139 |
+ Bear in mind that whenever the source code or the configuration files are
|
|
140 |
+ updated, you **must** re-build the image.
|
|
141 |
+ |
|
142 |
+ |
|
143 |
+.. _install-docker-compose:
|
|
111 | 144 |
|
112 |
-After building the Docker image, to run BuildGrid using the default configuration file
|
|
113 |
-(found in `buildgrid/_app/settings/default.yml`), simply run:
|
|
145 |
+Docker Compose
|
|
146 |
+~~~~~~~~~~~~~~
|
|
147 |
+ |
|
148 |
+BuildGrid ships a ``docker-compose.yml`` manifest for building and running a
|
|
149 |
+grid locally using ``docker-compose``. In order to produce a
|
|
150 |
+``buildgrid:local`` base image, run:
|
|
114 | 151 |
|
115 | 152 |
.. code-block:: sh
|
116 | 153 |
|
117 |
- docker run -i -p 50051:50051 buildgrid_server
|
|
154 |
+ git clone https://gitlab.com/BuildGrid/buildgrid.git
|
|
155 |
+ cd buildgrid
|
|
156 |
+ docker-compose build
|
|
157 |
+ |
|
158 |
+Once completed, you can start a minimal grid by running:
|
|
159 |
+ |
|
160 |
+.. code-block:: sh
|
|
161 |
+ |
|
162 |
+ docker-compose up
|
|
118 | 163 |
|
119 | 164 |
.. note::
|
120 | 165 |
|
121 |
- To run BuildGrid using a different configuration file, include the relative path to the
|
|
122 |
- configuration file at the end of the command above. For example, to run the default
|
|
123 |
- standalone CAS server (without an execution service), simply run:
|
|
166 |
+ The grid is composed of three containers:
|
|
167 |
+ |
|
168 |
+ - An execution service available at ``http://localhost:50051``.
|
|
169 |
+ - An CAS and action-cache service available at ``http://localhost:50052``.
|
|
170 |
+ - A single ``local`` instance with one host-tools based worker bot attached.
|
|
124 | 171 |
|
125 |
- .. code-block:: sh
|
|
172 |
+.. hint::
|
|
173 |
+ |
|
174 |
+ You can spin up more bots by using ``docker-compose`` scaling capabilities:
|
|
175 |
+ |
|
176 |
+ .. code-block:: sh
|
|
177 |
+ |
|
178 |
+ docker-compose up --scale bots=12
|
|
179 |
+ |
|
180 |
+.. hint::
|
|
126 | 181 |
|
127 |
- docker run -i -p 50052:50052 buildgrid_server buildgrid/_app/settings/cas.yml
|
|
182 |
+ The contained services configuration files are bind mounted into the
|
|
183 |
+ container, no need to rebuild the base image on configuration update.
|
|
184 |
+ Configuration files are read from:
|
|
128 | 185 |
|
186 |
+ - ``data/config/controller.conf`` for the execution service.
|
|
187 |
+ - ``data/config/storage.conf`` for the CAS and action-cache service.
|
... | ... | @@ -27,7 +27,7 @@ This defines a single ``main`` instance of the ``CAS``, ``Bytestream`` and ``Ref |
27 | 27 |
|
28 | 28 |
.. code-block:: sh
|
29 | 29 |
|
30 |
- bgd server start example.conf
|
|
30 |
+ bgd server start data/config/artifacts.conf
|
|
31 | 31 |
|
32 | 32 |
The server should now be available to use.
|
33 | 33 |
|
... | ... | @@ -17,7 +17,7 @@ In one terminal, start a server: |
17 | 17 |
|
18 | 18 |
.. code-block:: sh
|
19 | 19 |
|
20 |
- bgd server start buildgrid/_app/settings/default.yml
|
|
20 |
+ bgd server start data/config/default.conf
|
|
21 | 21 |
|
22 | 22 |
In another terminal, upload an action to CAS:
|
23 | 23 |
|
... | ... | @@ -78,7 +78,7 @@ Now start a BuildGrid server, passing it a directory it can write a CAS to: |
78 | 78 |
|
79 | 79 |
.. code-block:: sh
|
80 | 80 |
|
81 |
- bgd server start buildgrid/_app/settings/default.yml
|
|
81 |
+ bgd server start data/config/default.conf
|
|
82 | 82 |
|
83 | 83 |
Start the following bot session:
|
84 | 84 |
|
1 |
+cryptography >= 1.8.0 # Required by pyjwt for RSA
|
|
2 |
+pyjwt >= 1.5.0
|
1 |
+sphinx
|
|
2 |
+sphinx-click
|
|
3 |
+sphinx-rtd-theme >= 0.4.2 # For HTML search fix (upstream #672)
|
|
4 |
+sphinxcontrib-apidoc
|
|
5 |
+sphinxcontrib-napoleon
|
1 |
+coverage >= 4.5.0
|
|
2 |
+moto < 1.3.7
|
|
3 |
+pep8
|
|
4 |
+psutil
|
|
5 |
+pytest >= 3.8.0
|
|
6 |
+pytest-cov >= 2.6.0
|
|
7 |
+pytest-pep8
|
|
8 |
+pytest-pylint
|
1 |
+boto3 < 1.8.0 # For moto compatibility (BuildGrid/buildgrid#65)
|
|
2 |
+botocore < 1.11.0 # For moto compatibility (BuildGrid/buildgrid#65)
|
|
3 |
+click
|
|
4 |
+grpcio
|
|
5 |
+janus
|
|
6 |
+protobuf >= 3.6.1
|
|
7 |
+pyyaml
|
... | ... | @@ -77,63 +77,47 @@ class BuildGRPC(Command): |
77 | 77 |
f.write(code)
|
78 | 78 |
|
79 | 79 |
|
80 |
-def get_cmdclass():
|
|
81 |
- cmdclass = {
|
|
82 |
- 'build_grpc': BuildGRPC,
|
|
83 |
- }
|
|
84 |
- return cmdclass
|
|
85 |
- |
|
86 |
-auth_require = [
|
|
87 |
- 'cryptography >= 1.8.0', # Required by pyjwt for RSA
|
|
88 |
- 'pyjwt >= 1.5.0',
|
|
89 |
-]
|
|
90 |
- |
|
91 |
-tests_require = [
|
|
92 |
- 'coverage >= 4.5.0',
|
|
93 |
- 'moto < 1.3.7',
|
|
94 |
- 'pep8',
|
|
95 |
- 'psutil',
|
|
96 |
- 'pytest >= 3.8.0',
|
|
97 |
- 'pytest-cov >= 2.6.0',
|
|
98 |
- 'pytest-pep8',
|
|
99 |
- 'pytest-pylint',
|
|
100 |
-]
|
|
101 |
- |
|
102 |
-docs_require = [
|
|
103 |
- 'sphinx',
|
|
104 |
- 'sphinx-click',
|
|
105 |
- 'sphinx-rtd-theme >= 0.4.2', # For HTML search fix (upstream #672)
|
|
106 |
- 'sphinxcontrib-apidoc',
|
|
107 |
- 'sphinxcontrib-napoleon',
|
|
108 |
-]
|
|
80 |
+# Load main requirements from file:
|
|
81 |
+with open('requirements.txt') as requirements_file:
|
|
82 |
+ install_requirements = requirements_file.read().splitlines()
|
|
83 |
+ |
|
84 |
+auth_requirements = []
|
|
85 |
+# Load 'auth' requirements from dedicated file:
|
|
86 |
+if os.path.isfile('requirements.auth.txt'):
|
|
87 |
+ with open('requirements.auth.txt') as requirements_file:
|
|
88 |
+ auth_requirements = requirements_file.read().splitlines()
|
|
89 |
+ |
|
90 |
+docs_requirements = []
|
|
91 |
+# Load 'docs' requirements from dedicated file:
|
|
92 |
+if os.path.isfile('requirements.docs.txt'):
|
|
93 |
+ with open('requirements.docs.txt') as requirements_file:
|
|
94 |
+ docs_requirements = requirements_file.read().splitlines()
|
|
95 |
+ |
|
96 |
+tests_requirements = []
|
|
97 |
+# Load 'tests' requirements from dedicated file:
|
|
98 |
+if os.path.isfile('requirements.tests.txt'):
|
|
99 |
+ with open('requirements.tests.txt') as requirements_file:
|
|
100 |
+ tests_requirements = requirements_file.read().splitlines()
|
|
109 | 101 |
|
110 | 102 |
setup(
|
111 | 103 |
name="BuildGrid",
|
112 | 104 |
version=__version__,
|
113 |
- cmdclass=get_cmdclass(),
|
|
114 | 105 |
license="Apache License, Version 2.0",
|
115 | 106 |
description="A remote execution service",
|
107 |
+ cmdclass={
|
|
108 |
+ 'build_grpc': BuildGRPC, },
|
|
116 | 109 |
packages=find_packages(),
|
117 | 110 |
python_requires='>= 3.5.3', # janus requirement
|
118 |
- install_requires=[
|
|
119 |
- 'boto3 < 1.8.0',
|
|
120 |
- 'botocore < 1.11.0',
|
|
121 |
- 'click',
|
|
122 |
- 'grpcio',
|
|
123 |
- 'janus',
|
|
124 |
- 'protobuf >= 3.6.1',
|
|
125 |
- 'pyyaml',
|
|
126 |
- ],
|
|
111 |
+ install_requires=install_requirements,
|
|
112 |
+ setup_requires=['pytest-runner'],
|
|
113 |
+ tests_require=tests_requirements,
|
|
114 |
+ extras_require={
|
|
115 |
+ 'auth': auth_requirements,
|
|
116 |
+ 'docs': docs_requirements,
|
|
117 |
+ 'tests': tests_requirements, },
|
|
127 | 118 |
entry_points={
|
128 | 119 |
'console_scripts': [
|
129 | 120 |
'bgd = buildgrid._app:cli',
|
130 | 121 |
]
|
131 |
- },
|
|
132 |
- setup_requires=['pytest-runner'],
|
|
133 |
- tests_require=tests_require,
|
|
134 |
- extras_require={
|
|
135 |
- 'auth': auth_require,
|
|
136 |
- 'docs': docs_require,
|
|
137 |
- 'tests': tests_require,
|
|
138 |
- },
|
|
122 |
+ }
|
|
139 | 123 |
)
|