Skip to content

Commit d77d62d

Browse files
committed
fixes naming when project has multiple requirements.txt files
1 parent d59e29d commit d77d62d

File tree

10 files changed

+177
-69
lines changed

10 files changed

+177
-69
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# folder with examples using "kind" - we can delete it once we moved them to docs
2+
kind
3+
14
*.sublime-project
25
*.sublime-workspace
36
*.source

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[flake8]
2-
exclude = build/, src/soopervisor/assets/aws-lambda/app.py, tests/assets/fast-pipeline/fast_pipeline.py
2+
# kind is a tmp directory with tutorials, we can delete it once we move their content to the docs
3+
exclude = build/, src/soopervisor/assets/aws-lambda/app.py, tests/assets/fast-pipeline/fast_pipeline.py, kind/

src/soopervisor/aws/batch.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,15 @@ def _submit_dag(
144144
# Register job definitions for task specific images
145145
for pattern, image in image_map.items():
146146
if pattern != default_image_key:
147-
container_properties['image'] = image
147+
container_props = container_properties.copy()
148+
container_props['image'] = image
148149
task_job_def = f"{job_def}-{docker.modify_wildcard(pattern)}"
149150
cmdr.info(f'Registering {task_job_def!r} job definition...')
150151

151152
jd = client.register_job_definition(
152153
jobDefinitionName=task_job_def,
153154
type='container',
154-
containerProperties=container_properties)
155+
containerProperties=container_props)
155156
jd_map[pattern] = jd
156157

157158
for name, upstream in tasks.items():
@@ -280,6 +281,7 @@ def _export(cls, cfg, env_name, mode, until, skip_tests, skip_docker,
280281

281282
# add a unique suffix to prevent collisions
282283
suffix = str(uuid4())[:8]
284+
283285
cls._submit_dag(tasks=tasks,
284286
args=cli_args,
285287
job_def=f'{pkg_name}-{suffix}',

src/soopervisor/commons/docker.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
import shlex
13
import tarfile
24
from pathlib import Path
35

@@ -50,21 +52,42 @@ def get_dependencies():
5052
return dependency_files, lock_paths
5153

5254

53-
def build_image(e, cfg, env_name, until, entry_point, skip_tests, ignore_git,
54-
pkg_name, version, task):
55+
def build_image(
56+
e,
57+
cfg,
58+
env_name,
59+
until,
60+
entry_point,
61+
skip_tests,
62+
ignore_git,
63+
pkg_name,
64+
version,
65+
task,
66+
):
67+
"""Build Docker image
68+
69+
Parameters
70+
----------
71+
pkg_name : str
72+
Project name, used to tag the Docker image (e.g. {pkg_name}:{version})
73+
74+
version : str
75+
Project version, user to tag the Docker image
76+
(e.g. {pkg_name}:{version})
77+
"""
5578

5679
e.cp('dist')
5780

5881
e.cd(env_name)
5982

6083
if task:
61-
image_local = f'{pkg_name}:{version}-{modify_wildcard(task)}'
62-
63-
import os
64-
import shlex
84+
suffix = '' if task == 'default' else f'-{modify_wildcard(task)}'
85+
image_local = f'{pkg_name}{suffix}:{version}'
6586

6687
args = ['docker', 'build', '.', '--tag', image_local]
6788

89+
# NOTE: this is used in Ploomber Cloud, but it isn't documented in
90+
# soopervisor's docs
6891
if 'DOCKER_ARGS' in os.environ:
6992
args = args + shlex.split(os.environ['DOCKER_ARGS'])
7093

@@ -111,9 +134,11 @@ def build_image(e, cfg, env_name, until, entry_point, skip_tests, ignore_git,
111134

112135
if cfg.repository:
113136
image_target = cfg.repository
137+
114138
# Adding the latest tag if not a remote repo
115139
if ":" not in image_target:
116-
image_target = f'{image_target}:{version}-{modify_wildcard(task)}'
140+
image_target = f'{image_target}{suffix}:{version}'
141+
117142
e.run('docker',
118143
'tag',
119144
image_local,
@@ -176,7 +201,8 @@ def build(e,
176201

177202
# Generate source distribution
178203
if setup_flow:
179-
for task_pattern in list(dependency_files.keys()):
204+
for task_pattern in sorted(dependency_files.keys()):
205+
180206
if task_pattern != 'default':
181207
raise NotImplementedError(
182208
"Multiple requirements.*.lock.txt or "
@@ -188,9 +214,11 @@ def build(e,
188214
e.cp('requirements.lock.txt')
189215
elif Path('environment.lock.yml').exists():
190216
e.cp('environment.lock.yml')
217+
191218
e.rm('dist', 'build', Path('src', pkg_name, f'{pkg_name}.egg-info'))
192219
e.run('python', '-m', 'build', '--sdist', description='Packaging code')
193220
default_image_key = dependencies.get_default_image_key()
221+
194222
image = build_image(e,
195223
cfg,
196224
env_name,
@@ -206,7 +234,10 @@ def build(e,
206234
# raise error if include is not None? and suggest to use MANIFEST.in
207235
# instead
208236
else:
209-
for task, lock_file in lock_paths.items():
237+
# sort keys so we iterate in deterministic order and can test easily
238+
for task in sorted(lock_paths.keys()):
239+
lock_file = lock_paths[task]
240+
210241
if Path(lock_file).exists():
211242
e.cp(lock_file)
212243
e.rm('dist')

tests/airflow/test_airflow_export.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,5 @@ def test_export_airflow_callables(monkeypatch, mock_docker_calls_callables,
174174
assert td['fit'].arguments == [template.format('fit') + args]
175175
assert td['join'].arguments == [template.format('join') + args]
176176

177-
assert {t.image
178-
for t in td.values()} == {'your-repository/name:latest-default'}
177+
assert {t.image for t in td.values()} == {'your-repository/name:latest'}
179178
assert {tuple(t.cmds) for t in td.values()} == {('bash', '-cx')}

tests/argo/test_argo_export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def test_export(mock_docker_my_project, backup_packaged_project, monkeypatch,
6262
assert run_task_template['script']['workingDir'] is None
6363

6464
assert run_task_template['script'][
65-
'image'] == 'your-repository/name:0.1dev-default'
65+
'image'] == 'your-repository/name:0.1dev'
6666
assert run_task_template['name'] == 'run-task'
6767
assert spec['metadata']['generateName'] == 'my-project-'
6868
assert all([

tests/aws_batch/test_export.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,13 @@ def test_export_multiple_images_job_details(
329329
assert job_images == {
330330
'arn:aws:batch:us-east-1:123456789012:job-definition/'
331331
'multiple_requirements_project-uuid4:1':
332-
'your-repository/name:latest-default',
332+
'your-repository/name:latest',
333333
'arn:aws:batch:us-east-1:123456789012:job-definition/'
334334
'multiple_requirements_project-uuid4-clean-ploomber:1':
335-
'your-repository/name:latest-clean-ploomber',
335+
'your-repository/name-clean-ploomber:latest',
336336
'arn:aws:batch:us-east-1:123456789012:job-definition/'
337337
'multiple_requirements_project-uuid4-plot-ploomber:1':
338-
'your-repository/name:latest-plot-ploomber',
338+
'your-repository/name-plot-ploomber:latest',
339339
}
340340

341341

@@ -410,8 +410,8 @@ def test_checks_the_right_spec(mock_batch, mock_docker_my_project_serve,
410410
exporter.add()
411411
exporter.export(mode='incremental')
412412

413-
expected = ('docker', 'run', 'my_project:0.1dev-default', 'ploomber',
414-
'status', '--entry-point',
413+
expected = ('docker', 'run', 'my_project:0.1dev', 'ploomber', 'status',
414+
'--entry-point',
415415
str(Path('src', 'my_project', 'pipeline.serve.yaml')))
416416
assert mock_docker_my_project_serve.calls[2] == expected
417417

tests/conftest.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@ class CustomCommander(_commander.Commander):
2727
CustomCommander.run('docker', ...)
2828
"""
2929

30+
def __init__(self, *args, **kwargs):
31+
super().__init__(*args, **kwargs)
32+
self.docker_cmds = []
33+
3034
def run(self, *args, **kwargs):
3135
if args[0] == 'docker':
3236
print(f'ignoring: {args} {kwargs}')
37+
self.docker_cmds.append([args, kwargs])
3338
else:
3439
return super().run(*args, **kwargs)
3540

@@ -47,6 +52,7 @@ def git_init():
4752

4853
# mocks subprocess.run()
4954
def subprocess_run_mock(tester):
55+
5056
def ret(cmd, capture_output):
5157
result = tester(cmd)
5258

@@ -63,7 +69,7 @@ def ret(cmd, capture_output):
6369
return ret
6470

6571

66-
def _mock_commander_calls(monkeypatch, cmd, proj, tag, task='default'):
72+
def _mock_commander_calls(monkeypatch, cmd, proj, tag, task=None):
6773
"""
6874
Mock subprocess calls made by ploomber.io._commander.Commander (which
6975
is used interally by the soopervisor.commons.docker module).
@@ -73,12 +79,14 @@ def _mock_commander_calls(monkeypatch, cmd, proj, tag, task='default'):
7379
to "python -m build --sdist" to pass. For details, see the CommanderTester
7480
docstring in the ploomber package.
7581
"""
82+
suffix = '' if task is None else f'-{task}'
83+
7684
tester = _commander_tester.CommanderTester(
7785
run=[
7886
('python', '-m', 'build', '--sdist'),
7987
],
8088
return_value={
81-
('docker', 'run', f'{proj}:{tag}-{task}', 'python', '-c', cmd):
89+
('docker', 'run', f'{proj}{suffix}:{tag}', 'python', '-c', cmd):
8290
b'True\n'
8391
})
8492

tests/test_commons.py

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,47 @@ def test_docker_build(tmp_sample_project):
626626

627627
assert existing == expected
628628

629+
# check tag name
630+
cmd = cmdr.docker_cmds[0][0]
631+
name, tag = cmd[1], cmd[-1]
632+
assert name == 'build' and tag == 'sample_project:latest'
633+
634+
635+
@pytest.mark.parametrize('repo, expected', [
636+
['docker.company.com/something', 'docker.company.com/something:latest'],
637+
['docker.company.com/something:v2', 'docker.company.com/something:v2'],
638+
])
639+
def test_docker_build_with_repository(tmp_sample_project, repo, expected):
640+
Path('some-env').mkdir()
641+
Path('some-env', 'Dockerfile').touch()
642+
643+
cfg = ConcreteDockerConfig(repository=repo)
644+
645+
with CustomCommander(workspace='some-env') as cmdr:
646+
commons.docker.build(cmdr,
647+
cfg,
648+
'some-env',
649+
until=None,
650+
entry_point='pipeline.yaml')
651+
# check tag name
652+
cmd = cmdr.docker_cmds[0][0]
653+
assert cmd == (
654+
'docker',
655+
'build',
656+
'.',
657+
'--tag',
658+
'sample_project:latest',
659+
)
660+
661+
# check tag command
662+
cmd = cmdr.docker_cmds[-2][0]
663+
assert cmd == (
664+
'docker',
665+
'tag',
666+
'sample_project:latest',
667+
expected,
668+
)
669+
629670

630671
def test_docker_build_multiple_requirement(
631672
tmp_sample_project_multiple_requirement):
@@ -639,11 +680,53 @@ def test_docker_build_multiple_requirement(
639680
'some-env',
640681
until=None,
641682
entry_point='pipeline.yaml')
683+
684+
# validate docker is called with the right arguments
685+
docker_args = [args[0] for args in cmdr.docker_cmds]
686+
687+
def generate_commands(suffix):
688+
image_name = f'multiple_requirements_project{suffix}:latest'
689+
690+
return [
691+
# build image
692+
(
693+
'docker',
694+
'build',
695+
'.',
696+
'--tag',
697+
image_name,
698+
),
699+
# check pipeline load
700+
(
701+
'docker',
702+
'run',
703+
image_name,
704+
'ploomber',
705+
'status',
706+
'--entry-point',
707+
'pipeline.yaml',
708+
),
709+
# check pipeline has a client
710+
(
711+
'docker',
712+
'run',
713+
image_name,
714+
'python',
715+
'-c',
716+
('from ploomber.spec import DAGSpec; '
717+
'print("File" in DAGSpec("pipeline.yaml").to_dag().clients)'),
718+
)
719+
]
720+
721+
assert generate_commands('-clean-ploomber') == docker_args[:3]
722+
assert generate_commands('') == docker_args[3:6]
723+
assert generate_commands('-plot-ploomber') == docker_args[6:]
724+
642725
assert pkg_name == 'multiple_requirements_project'
643726
assert image_map == \
644-
{'default': 'multiple_requirements_project:latest-default',
645-
'clean-*': 'multiple_requirements_project:latest-clean-ploomber',
646-
'plot-*': 'multiple_requirements_project:latest-plot-ploomber'}
727+
{'default': 'multiple_requirements_project:latest',
728+
'clean-*': 'multiple_requirements_project-clean-ploomber:latest',
729+
'plot-*': 'multiple_requirements_project-plot-ploomber:latest'}
647730

648731
existing = _list_files(Path('dist',
649732
'multiple_requirements_project.tar.gz'))

0 commit comments

Comments
 (0)