Skip to content

Commit e39494b

Browse files
author
Rodrigo Valin
authored
CLOUDP-80849: use temporary files for docker templating (#25)
1 parent a980700 commit e39494b

File tree

8 files changed

+253
-31
lines changed

8 files changed

+253
-31
lines changed

README.md

+12-13
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ build Docker images.
1414

1515
Sonar can be used as a Python module or as a standalone program. Sonar will look
1616
for an `inventory.yaml` file in your local directory that should contain a
17-
collection of images to build and stages for each one of those images.
17+
collection of images to build and stages for each one of those images. A
18+
different inventory file can be specified using `--inventory <file-path>`.
1819

1920
Sonar comes with an inventory file to be able to build itself, and to run its
20-
unit tests. This [inventory.yaml](inventory.yaml) is:
21+
unit tests. This [simple.yaml](inventories/simple.yaml) is:
2122

2223
``` yaml
2324
vars:
24-
# start a local Docker registry with:
25+
# start a local registry with:
2526
# docker run -d -p 5000:5000 --restart=always --name registry registry:2
2627
registry: localhost:5000
2728

@@ -49,32 +50,30 @@ images:
4950
task_type: tag_image
5051

5152
source:
52-
registry: $(inputs.params.registry)/sonar-tester-image
53-
tag: $(inputs.params.version_id)
53+
registry: $(stages['build-sonar-tester-image'].output[0].registry)
54+
tag: $(stages['build-sonar-tester-image'].output[0].tag)
5455

5556
destination:
56-
- registry: $(inputs.params.registry)/sonar-tester-image-copy
57-
tag: $(inputs.params.version_id)
58-
57+
- registry: $(inputs.params.registry)/sonar-tester-image
58+
tag: latest
5959
```
6060
6161
To execute this inventory file, you can do:
6262
6363
```
64-
$ python sonar.py --image sonar-test-runner
64+
$ python sonar.py --image sonar-test-runner --inventory inventories/simple.yaml
6565

6666
[build-sonar-tester-image/docker_build] stage-started build-sonar-tester-image: 1/2
6767
[build-sonar-tester-image/docker_build] docker-image-push: localhost:5000/sonar-tester-image:8945563b-248e-4c03-bb0a-6cc15cff1a6e
6868
[tag-image/tag_image] stage-started tag-image: 2/2
69-
[tag-image/tag_image] docker-image-push: localhost:5000/sonar-tester-image-copy:8945563b-248e-4c03-bb0a-6cc15cff1a6e
69+
[tag-image/tag_image] docker-image-push: localhost:5000/sonar-tester-image:latest
7070
```
7171
7272
At the end of this phase, you'll have a Docker image tagged as
73-
`localhost:5000/sonar-tester-image:8945563b-248e-4c03-bb0a-6cc15cff1a6e` that
74-
you will be able to run with:
73+
`localhost:5000/sonar-tester-image:latest` that you will be able to run with:
7574

7675
```
77-
$ docker run localhost:5000/sonar-tester-image:799170de-74a0-4310-a674-d704b83f2ed2
76+
$ docker run localhost:5000/sonar-tester-image:latest
7877
============================= test session starts ==============================
7978
platform linux -- Python 3.9.4, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
8079
rootdir: /src

docker/Dockerfile.3.10rc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% extends "Dockerfile.template" %}
2+
3+
{% set from_base = "python:3.10-rc-slim" %}
4+
5+
# Sets the base version as 3.10 release candidate to try Sonar on latest Python!

docker/Dockerfile.template

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM {{ from_base|default("python:3.9-slim") }}
2+
3+
COPY . /src
4+
5+
RUN pip install -r /src/requirements.txt
6+
7+
WORKDIR /src/
8+
ENTRYPOINT pytest

inventories/inventory-template.yaml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## This is a more complex inventory file. It has a few features on it:
2+
##
3+
## 1. A dockerfile can be a Jinja2 template, like `docker/Dockerfile.template`
4+
## 2. This template dockerfile gets rendered into a concrete Dockerfile in a
5+
## temp file on disk, using the $(functions.tempfile) function.
6+
## 3. The name of this tempfile is passed further and used by a subsequent
7+
## stage using the `$(stages['stage-name'].outputs[0].dockerfile)`
8+
##
9+
## To run this inventory you have to:
10+
##
11+
## ./sonar.py --image sonar-test-runner --inventory inventories/inventory-template.yaml
12+
##
13+
14+
vars:
15+
# start a local registry with:
16+
# docker run -d -p 5000:5000 --restart=always --name registry registry:2
17+
registry: localhost:5000
18+
19+
images:
20+
- name: sonar-test-runner
21+
22+
vars:
23+
template_context: docker
24+
context: .
25+
26+
# First stage builds a Docker image. The resulting image will be
27+
# pushed to the registry in the `output` section.
28+
stages:
29+
30+
- name: template-sonar
31+
task_type: dockerfile_template
32+
template_file_extension: 3.10rc # Template will be `Dockerfile.3.10rc`
33+
34+
output:
35+
# We will use $(functions.tempfile) to use a temporary file. The name of the
36+
# temporary file will have to be accessed using
37+
# `$(stages['stage-name']).outputs` afterwards.
38+
- dockerfile: $(functions.tempfile)
39+
40+
- name: build-sonar-tester-image
41+
task_type: docker_build
42+
43+
dockerfile: $(stages['template-sonar'].outputs[0].dockerfile)
44+
45+
output:
46+
- registry: $(inputs.params.registry)/sonar-template-test
47+
tag: $(inputs.params.version_id)
48+
- registry: $(inputs.params.registry)/sonar-template-test
49+
tag: latest

inventory.yaml renamed to inventories/simple.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ images:
2727
task_type: tag_image
2828

2929
source:
30-
registry: $(inputs.params.registry)/sonar-tester-image
31-
tag: $(inputs.params.version_id)
30+
registry: $(stages['build-sonar-tester-image'].outputs[0].registry)
31+
tag: $(stages['build-sonar-tester-image'].outputs[0].tag)
3232

3333
destination:
34-
- registry: $(inputs.params.registry)/sonar-tester-image-copy
35-
tag: $(inputs.params.version_id)
34+
- registry: $(inputs.params.registry)/sonar-tester-image
35+
tag: latest

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
setup(
66
name="sonar",
7-
version="0.0.10",
7+
version="0.0.11",
88
description="Sonar Docker Building Tools",
99
author="Rodrigo Valin",
1010
author_email="[email protected]",

sonar/sonar.py

+87-11
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
import subprocess
1212
import tempfile
1313
import uuid
14+
from collections import defaultdict
1415
from dataclasses import dataclass, field
1516
from pathlib import Path
1617
from shutil import copyfile
17-
from typing import Dict, List, Optional, Tuple, Union
18+
from typing import Dict, List, Optional, Tuple, Union, Any
1819
from urllib.request import urlretrieve
1920

2021
import boto3
@@ -79,6 +80,12 @@ class Context:
7980
# Generates a version_id to use if one is not present
8081
stored_version_id: str = str(uuid.uuid4())
8182

83+
# stage_outputs is a dictionary of dictionaries. First dict
84+
# has a key corresponding to the name of the stage, the dict
85+
# you get from it is key/value (str, Any) with the values
86+
# stored by given stage.
87+
stage_outputs: Dict[str, List[Dict[str, Any]]] = field(default_factory=dict)
88+
8289
# pylint: disable=C0103
8390
def I(self, string):
8491
"""
@@ -101,6 +108,16 @@ def version_id(self):
101108
return os.environ.get("version_id", self.stored_version_id)
102109

103110

111+
def append_output_in_context(ctx: Context, stage_name: str, values: Dict) -> None:
112+
"""Stores a value as the output of the stage, so it can be consumed by future stages."""
113+
if stage_name not in ctx.stage_outputs.keys():
114+
# adds a new empty dictionary to this stage
115+
# if there isn't one yet.
116+
ctx.stage_outputs[stage_name] = list()
117+
118+
ctx.stage_outputs[stage_name].append(values)
119+
120+
104121
def find_inventory(inventory: Optional[str] = None):
105122
"""
106123
Finds the inventory file, and return it as a yaml object.
@@ -124,14 +141,6 @@ def find_image(image_name: str, inventory: str):
124141
raise ValueError("Image {} not found".format(image_name))
125142

126143

127-
def find_variables_to_interpolate(string) -> List[str]:
128-
"""
129-
Returns a list of variables in the string that need to be interpolated.
130-
"""
131-
var_finder_re = r"\$\(inputs\.params\.(?P<var>\w+)\)"
132-
return re.findall(var_finder_re, string, re.UNICODE)
133-
134-
135144
def find_variable_replacement(ctx: Context, variable: str, stage=None) -> str:
136145
"""
137146
Returns the variable *value* for this varable.
@@ -187,6 +196,38 @@ def find_variable_replacements(
187196
return replacements
188197

189198

199+
def execute_interpolatable_function(name: str) -> str:
200+
if name == "tempfile":
201+
tmp = tempfile.mkstemp()
202+
# mkstemp returns a tuple, with the second element of it being
203+
# the absolute path to the file.
204+
return tmp[1]
205+
206+
raise ValueError("Only supported function is 'tempfile'")
207+
208+
209+
def find_variables_to_interpolate_from_stage(string: str) -> List[Any]:
210+
"""Finds a $(stage['stage-name'].outputs[])"""
211+
var_finder_re = r"\$\(stages\[\'(?P<stage_name>[\w-]+)\'\]\.outputs\[(?P<index>\d+)\]\.(?P<key>\w+)"
212+
213+
return re.findall(var_finder_re, string, re.UNICODE)
214+
215+
216+
def find_variables_to_interpolate(string) -> List[str]:
217+
"""
218+
Returns a list of variables in the string that need to be interpolated.
219+
"""
220+
var_finder_re = r"\$\(inputs\.params\.(?P<var>\w+)\)"
221+
return re.findall(var_finder_re, string, re.UNICODE)
222+
223+
224+
def find_functions_to_interpolate(string: str) -> List[Any]:
225+
"""Find functions to be interpolated."""
226+
var_finder_re = r"\$\(functions\.(?P<var>\w+)\)"
227+
228+
return re.findall(var_finder_re, string, re.UNICODE)
229+
230+
190231
def interpolate_vars(ctx: Context, string: str, stage=None) -> str:
191232
"""
192233
For each variable to interpolate in string, finds its *value* and
@@ -200,8 +241,23 @@ def interpolate_vars(ctx: Context, string: str, stage=None) -> str:
200241
"$(inputs.params.{})".format(variable), replacements[variable]
201242
)
202243

203-
return string
244+
variables = find_variables_to_interpolate_from_stage(string)
245+
for stage, index, key in variables:
246+
value = ctx.stage_outputs[stage][int(index)][key]
247+
string = string.replace(
248+
"$(stages['{}'].outputs[{}].{})".format(stage, index, key),
249+
value
250+
)
204251

252+
functions = find_functions_to_interpolate(string)
253+
for name in functions:
254+
value = execute_interpolatable_function(name)
255+
string = string.replace(
256+
"$(functions.{})".format(name),
257+
value
258+
)
259+
260+
return string
205261

206262
def build_add_statement(ctx, block) -> str:
207263
"""
@@ -334,6 +390,11 @@ def task_tag_image(ctx: Context):
334390
else:
335391
raise
336392

393+
append_output_in_context(ctx, ctx.stage["name"], {
394+
"registry": registry,
395+
"tag": tag
396+
})
397+
337398

338399
def get_rendering_params(ctx: Context) -> Dict[str, str]:
339400
"""
@@ -532,6 +593,11 @@ def task_docker_build(ctx: Context):
532593
else:
533594
raise
534595

596+
append_output_in_context(ctx, ctx.stage["name"], {
597+
"registry": registry,
598+
"tag": tag,
599+
})
600+
535601
if sign:
536602
clear_signing_environment(signing_key_name)
537603

@@ -568,7 +634,13 @@ def task_dockerfile_template(ctx: Context):
568634
except KeyError:
569635
pass
570636

571-
dockerfile = run_dockerfile_template(ctx, template_context, ctx.stage.get("distro"))
637+
638+
template_file_extension = ctx.stage.get("template_file_extension")
639+
if template_file_extension is None:
640+
# Use distro as compatibility with pre 0.11
641+
template_file_extension = ctx.stage.get("distro")
642+
643+
dockerfile = run_dockerfile_template(ctx, template_context, template_file_extension)
572644

573645
for output in ctx.stage["output"]:
574646
if "dockerfile" in output:
@@ -577,6 +649,10 @@ def task_dockerfile_template(ctx: Context):
577649

578650
echo(ctx, "dockerfile-save-location", output_dockerfile)
579651

652+
append_output_in_context(ctx, ctx.stage["name"], {
653+
"dockerfile": output_dockerfile
654+
})
655+
580656

581657
def find_skip_tags(params: Optional[Dict[str, str]] = None) -> List[str]:
582658
"""Returns a list of tags passed in params that should be excluded from the build."""

0 commit comments

Comments
 (0)