cs-image-system-docker-runtime-plugin 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cs_image_system/docker_runtime/__init__.py +3 -0
- cs_image_system/docker_runtime/bootstrap.py +129 -0
- cs_image_system/docker_runtime/docker_names.py +136 -0
- cs_image_system/docker_runtime/docker_packer_source.py +140 -0
- cs_image_system/docker_runtime/docker_provider_specific_image.py +40 -0
- cs_image_system/docker_runtime/docker_runtime_builders.py +574 -0
- cs_image_system/docker_runtime/docker_runtime_models.py +110 -0
- cs_image_system/docker_runtime/main.py +27 -0
- cs_image_system_docker_runtime_plugin-0.1.6.dist-info/METADATA +268 -0
- cs_image_system_docker_runtime_plugin-0.1.6.dist-info/RECORD +13 -0
- cs_image_system_docker_runtime_plugin-0.1.6.dist-info/WHEEL +4 -0
- cs_image_system_docker_runtime_plugin-0.1.6.dist-info/entry_points.txt +5 -0
- cs_image_system_docker_runtime_plugin-0.1.6.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""The container-host section of the bootstrap (stage 103 item 15).
|
|
6
|
+
|
|
7
|
+
A read-and-say section with no terraform module, because by decision
|
|
8
|
+
nothing in it can be created: a docker daemon is a host somebody runs, and
|
|
9
|
+
whoever reaches it is root on that host. What the section does instead is
|
|
10
|
+
read the tree's docker runtime from the raw ``cfg/`` (never loading it, and
|
|
11
|
+
never printing an address: a ``host`` may be an ``ENC[age:...]`` marker) and
|
|
12
|
+
ask the one question the rest of the bootstrap needs answered, *can CI reach
|
|
13
|
+
the daemon?*
|
|
14
|
+
|
|
15
|
+
* **Yes** (an ``ssh://`` host, or the environment's daemon with an address
|
|
16
|
+
in the workflow, the transports the starters' workflow reaches): the two
|
|
17
|
+
repository secrets that workflow reads to reach it, the ssh private key and
|
|
18
|
+
the host's ``known_hosts`` line (``DOCKER_HOST_SSH_KEY`` and
|
|
19
|
+
``DOCKER_HOST_KNOWN_HOSTS``, in the GitHub section's script with the rest),
|
|
20
|
+
are set from files, and the section says that neither is made here. The key
|
|
21
|
+
belongs to a user on the host who may use the daemon; it is made on your
|
|
22
|
+
machine, never by the system.
|
|
23
|
+
* **No** (a local socket, a context, a ``tcp://`` host, or a host the YAML
|
|
24
|
+
keeps as a marker): CI never performs on the runtime, and the section says
|
|
25
|
+
to name it in ``GUARD_RUNTIME`` so a change to its declaration fails CI
|
|
26
|
+
loudly before anything runs (the GitHub section's default for that
|
|
27
|
+
variable already names a runtime CI cannot reach).
|
|
28
|
+
|
|
29
|
+
The section also records what it does NOT make, in the audit the README
|
|
30
|
+
tabulates: the daemon, the network it names, the docker CLI on the runner.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from cs_image_system.base.bootstrap.facts import Facts, ci_can_reach, real, runtime_of_type
|
|
38
|
+
from cs_image_system.base.bootstrap.questions import BY_HAND, READ, Answers, Question, Rendered, Section
|
|
39
|
+
|
|
40
|
+
RUNTIME_TYPE = "docker"
|
|
41
|
+
GUIDE = "CI_SETUP.md 3.4a"
|
|
42
|
+
|
|
43
|
+
def _runtime(facts: Facts) -> dict[str, Any]:
|
|
44
|
+
return runtime_of_type(facts, RUNTIME_TYPE) or {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _runtime_default(facts: Facts, answers: Answers) -> str | None:
|
|
48
|
+
name = _runtime(facts).get("name")
|
|
49
|
+
return str(name) if name else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _transport(runtime: dict[str, Any]) -> str:
|
|
53
|
+
"""How the runtime names its daemon, from the raw YAML: ``ssh``, ``tcp``,
|
|
54
|
+
``unix``, ``context``, ``marker`` (an encrypted host, unreadable here) or
|
|
55
|
+
``environment`` (neither: the daemon of the machine that runs the tool)."""
|
|
56
|
+
host = str(runtime.get("host") or "").strip()
|
|
57
|
+
if host.startswith("ENC["):
|
|
58
|
+
return "marker"
|
|
59
|
+
if host:
|
|
60
|
+
scheme = host.split("://", 1)[0].lower() if "://" in host else "unix"
|
|
61
|
+
return scheme if scheme in ("ssh", "tcp", "unix") else "unix"
|
|
62
|
+
return "context" if real(runtime.get("context")) else "environment"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _reach_default(facts: Facts, answers: Answers) -> bool:
|
|
66
|
+
return ci_can_reach(_runtime(facts))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
QUESTIONS: tuple[Question, ...] = (
|
|
70
|
+
Question("runtime", "The docker runtime this section is about (a `name:` from cfg/runtime-builders.yml)", "text",
|
|
71
|
+
default=_runtime_default,
|
|
72
|
+
help="no runtime of type docker could be read from cfg/"),
|
|
73
|
+
Question("ci_reaches", "Can CI reach the docker daemon (an ssh:// host the workflow can log in to)?", "bool",
|
|
74
|
+
default=_reach_default,
|
|
75
|
+
help="the starters' workflow reaches an ssh:// host only; say no for a socket, a context or a tcp:// host"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def render(answers: Answers, facts: Facts | None = None) -> Rendered:
|
|
80
|
+
name = str(answers.get("runtime") or "the docker runtime")
|
|
81
|
+
reaches = bool(answers.get("ci_reaches"))
|
|
82
|
+
transport = _transport(_runtime(facts)) if facts is not None else "environment"
|
|
83
|
+
by_hand: list[str] = [
|
|
84
|
+
f"The daemon of `{name}` is the team's: install, patch and secure it on its host; whoever can reach it is "
|
|
85
|
+
"root on that host, and the system says so rather than hiding it (CONFIGURATION.md section 4).",
|
|
86
|
+
]
|
|
87
|
+
if reaches:
|
|
88
|
+
by_hand.append(
|
|
89
|
+
"CI's way in: make an ed25519 key for a user on the docker host who may use the daemon "
|
|
90
|
+
f"(`ssh-keygen -t ed25519 -f DOCKER_HOST_SSH_KEY -N ''`), authorize its public half there, put the private "
|
|
91
|
+
"half in the secrets directory as `DOCKER_HOST_SSH_KEY`, and the host's key line (`ssh-keyscan -t ed25519 "
|
|
92
|
+
f"HOST`, checked against the host by another route) as `DOCKER_HOST_KNOWN_HOSTS` ({GUIDE}).")
|
|
93
|
+
else:
|
|
94
|
+
by_hand.append(
|
|
95
|
+
f"CI cannot reach `{name}`: name it in the `GUARD_RUNTIME` Actions variable (the GitHub section's default "
|
|
96
|
+
"does) so a change to its declaration fails CI before anything runs, and keep PERFORM_RUNTIME on a "
|
|
97
|
+
"runtime CI can reach, or leave the `perform` job unconfigured (it skips, and says so).")
|
|
98
|
+
return Rendered(
|
|
99
|
+
by_hand=tuple(by_hand),
|
|
100
|
+
notes=(
|
|
101
|
+
f"the docker runtime `{name}`: its daemon is named by "
|
|
102
|
+
+ {"ssh": "an ssh:// host", "tcp": "a tcp:// host", "unix": "a local socket", "context": "a docker context",
|
|
103
|
+
"marker": "an encrypted host", "environment": "the environment (DOCKER_HOST, else the local socket)"}.get(
|
|
104
|
+
transport, "the environment"),
|
|
105
|
+
"CI " + ("reaches it over ssh with DOCKER_HOST_SSH_KEY and DOCKER_HOST_KNOWN_HOSTS"
|
|
106
|
+
if reaches else "does not reach it: it is a runtime CI must never bake on"),
|
|
107
|
+
"nothing here is terraform: the section reads the declaration and says what remains",
|
|
108
|
+
),
|
|
109
|
+
coverage=(
|
|
110
|
+
(f"the daemon of `{name}`", BY_HAND, "a host somebody runs; the system never installs or configures a daemon"),
|
|
111
|
+
("the network the runtime names", BY_HAND, "referenced by name and never created or changed: the team's, by decision"),
|
|
112
|
+
("the docker CLI on the runner", READ, "the runner image carries it (CI_RUNNER.md); the workflow only checks it is there"),
|
|
113
|
+
("CI's key to the daemon's host" if reaches else "CI's performing run on the runtime",
|
|
114
|
+
BY_HAND,
|
|
115
|
+
"made on your machine and set by the script from a file; the system never makes a key" if reaches
|
|
116
|
+
else "not built: CI has no way into the daemon, so the runtime is guarded instead"),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def wanted(facts: Facts) -> bool:
|
|
122
|
+
"""The tree declares a docker runtime."""
|
|
123
|
+
return runtime_of_type(facts, RUNTIME_TYPE) is not None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def docker_section() -> Section:
|
|
127
|
+
return Section(name="docker", title="Container host", questions=QUESTIONS, render=render, wanted=wanted,
|
|
128
|
+
wants_facts=True,
|
|
129
|
+
description="the docker daemon CI can or cannot reach, the secrets that reach it, the runtime to guard")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""The spellings a container daemon accepts (stage 103), mapped ONCE here.
|
|
6
|
+
|
|
7
|
+
* a **repository** name is lowercase: ``[a-z0-9]`` with single ``.``, ``_``,
|
|
8
|
+
``__`` or ``-`` separators, path components joined by ``/``;
|
|
9
|
+
* a **tag** is ``[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}``: at most 128
|
|
10
|
+
characters, and a build's final name (``<prefix>-<timestamp>``) is written
|
|
11
|
+
as one, so a name that would be cut is refused rather than cut (the cut
|
|
12
|
+
would fall where the timestamp is);
|
|
13
|
+
* a **label** key is a string without whitespace or ``=``; the OCI convention
|
|
14
|
+
is lowercase reverse-DNS, but the system's ``csis_*`` keys and a tree's
|
|
15
|
+
own are accepted as written.
|
|
16
|
+
|
|
17
|
+
A build is identified by its IMAGE ID (``sha256:<64 hex>``), which tags
|
|
18
|
+
move around; the lineage build id on this runtime is that id.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
TAG_MAX = 128
|
|
25
|
+
_TAG_BAD = re.compile(r"[^A-Za-z0-9_.-]+")
|
|
26
|
+
_REPO_BAD = re.compile(r"[^a-z0-9._/-]+")
|
|
27
|
+
_IMAGE_ID = re.compile(r"^(sha256:)?[0-9a-f]{64}$")
|
|
28
|
+
_DIGEST_REF = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def tag_name(value: str) -> str:
|
|
32
|
+
"""A valid docker tag derived deterministically from ``value``: what it
|
|
33
|
+
cannot keep becomes ``-``, a separator may not lead. Not shortened."""
|
|
34
|
+
v = _TAG_BAD.sub("-", str(value)).strip(".-")
|
|
35
|
+
if not v:
|
|
36
|
+
return "i"
|
|
37
|
+
if not (v[0].isalnum() or v[0] == "_"):
|
|
38
|
+
v = f"i{v}"
|
|
39
|
+
return v
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def tag_problem(name: str) -> str | None:
|
|
43
|
+
"""Why ``name``, once made a tag, cannot be one: only its length."""
|
|
44
|
+
made = tag_name(name)
|
|
45
|
+
if len(made) > TAG_MAX:
|
|
46
|
+
return (f"a docker tag is at most {TAG_MAX} characters; {name!r} becomes {made!r} ({len(made)}) "
|
|
47
|
+
f"and would be cut where the timestamp is")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def repository_name(value: str) -> str:
|
|
52
|
+
"""A valid repository name derived from ``value``: lowercase, what it
|
|
53
|
+
cannot keep becomes ``-``, separators never doubled or leading."""
|
|
54
|
+
v = _REPO_BAD.sub("-", str(value).lower())
|
|
55
|
+
v = re.sub(r"[._-]{2,}", "-", v).strip("._-/")
|
|
56
|
+
return v or "csis"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def label_key_problem(key: str) -> str | None:
|
|
60
|
+
"""Why ``key`` cannot be a docker label key, or None."""
|
|
61
|
+
k = str(key)
|
|
62
|
+
if not k.strip():
|
|
63
|
+
return "a docker label key cannot be empty"
|
|
64
|
+
if any(c.isspace() for c in k):
|
|
65
|
+
return f"a docker label key carries no whitespace; {key!r} does"
|
|
66
|
+
if "=" in k:
|
|
67
|
+
return f"a docker label key carries no '='; {key!r} does"
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_image_id(value: str) -> bool:
|
|
72
|
+
return bool(_IMAGE_ID.match(str(value).strip()))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_digest_reference(value: str) -> bool:
|
|
76
|
+
"""``<repository>@sha256:<digest>``: the form ``image_id`` pins."""
|
|
77
|
+
return bool(_DIGEST_REF.match(str(value).strip()))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
#: The registry a daemon may put in front of a repository it was given
|
|
81
|
+
#: unqualified: Podman's API spells ``csis/x:t`` as ``docker.io/csis/x:t``
|
|
82
|
+
#: (or ``localhost/csis/x:t``) in ``RepoTags``, where Docker keeps it as
|
|
83
|
+
#: written. Found by the Podman leg (stage 103, 2026-11-01).
|
|
84
|
+
IMPLIED_REGISTRIES = ("docker.io/", "index.docker.io/", "localhost/")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def same_repository(reported: str, repository: str) -> bool:
|
|
88
|
+
"""Whether a ``RepoTags`` entry's repository part IS the runtime's
|
|
89
|
+
repository, however the daemon spells the registry it implied."""
|
|
90
|
+
if reported == repository:
|
|
91
|
+
return True
|
|
92
|
+
return any(reported.startswith(p) and reported[len(p):] == repository for p in IMPLIED_REGISTRIES)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def short_id(image_id: str) -> str:
|
|
96
|
+
"""The twelve hex characters the CLI shows for an image id."""
|
|
97
|
+
v = str(image_id).strip()
|
|
98
|
+
v = v.split(":", 1)[1] if v.startswith("sha256:") else v
|
|
99
|
+
return v[:12]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def full_id(image_id: str) -> str:
|
|
103
|
+
"""The ``sha256:``-prefixed form of an image id the CLI printed."""
|
|
104
|
+
v = str(image_id).strip()
|
|
105
|
+
return v if v.startswith("sha256:") else f"sha256:{v}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# tags that carry what a label cannot change after the build (stage 103,
|
|
109
|
+
# decision 4: labels are fixed when an image is built, so release and
|
|
110
|
+
# relabel MOVE TAGS): `<repository>:<short id>--<key>--<value as tag>`
|
|
111
|
+
MOVABLE_SEP = "--"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def movable_tag(image_id: str, key: str, value: str) -> str:
|
|
115
|
+
return f"{short_id(image_id)}{MOVABLE_SEP}{tag_name(key)}{MOVABLE_SEP}{tag_name(value)}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_ID_AS_TAG = re.compile(r"^sha256-([0-9a-f]{64})$")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def movable_value_decoded(value: str) -> str:
|
|
122
|
+
"""A movable tag's value as it was given: an image id written as a tag
|
|
123
|
+
lost its colon (``sha256-<hex>``), and gets it back; anything else is
|
|
124
|
+
returned as it stands (found live: the state query compared the
|
|
125
|
+
lineage's ``sha256:...`` parent with the tag's ``sha256-...``)."""
|
|
126
|
+
m = _ID_AS_TAG.match(str(value))
|
|
127
|
+
return f"sha256:{m.group(1)}" if m else str(value)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def parse_movable_tag(tag: str) -> tuple[str, str, str] | None:
|
|
131
|
+
"""``(short id, key, value)`` of a movable tag, or None for any other;
|
|
132
|
+
the value is decoded (``movable_value_decoded``)."""
|
|
133
|
+
parts = str(tag).split(MOVABLE_SEP, 2)
|
|
134
|
+
if len(parts) != 3 or not re.fullmatch(r"[0-9a-f]{12}", parts[0]) or not parts[1] or not parts[2]:
|
|
135
|
+
return None
|
|
136
|
+
return parts[0], parts[1], movable_value_decoded(parts[2])
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""The ``docker`` packer source for an image baked on a docker runtime
|
|
6
|
+
(stage 103), from HashiCorp's docker plugin.
|
|
7
|
+
|
|
8
|
+
A bake starts a container from the parent image, provisions it through the
|
|
9
|
+
daemon (no SSH: the plugin's docker communicator runs ``docker exec`` and
|
|
10
|
+
``docker cp``) and COMMITS it. The committed image carries the lineage
|
|
11
|
+
labels and the keepalive command as Dockerfile instructions (``changes``);
|
|
12
|
+
the build's names are tags, put on by the ``docker-tag`` post-processor the
|
|
13
|
+
runtime adds to the build block: the final name and the series' floating
|
|
14
|
+
tag, which a child image bakes from when its parent is deferred.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import shlex
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import hcl2
|
|
23
|
+
from hcl2 import Builder
|
|
24
|
+
|
|
25
|
+
from cs_image_system.contract.constants import OOPS_DEFAULTS, SELF
|
|
26
|
+
from cs_image_system.hashicorp_utils.hashicorp import FO
|
|
27
|
+
|
|
28
|
+
from .docker_names import is_image_id, tag_name
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
DOCKER_SOURCE = "docker"
|
|
33
|
+
#: PID 1's command in every image this runtime bakes (decision 2: no init
|
|
34
|
+
#: system; the launcher runs it under ``--init``)
|
|
35
|
+
KEEPALIVE_CMD = ["sleep", "infinity"]
|
|
36
|
+
#: where the bake writes what must not land in the image (the step runner)
|
|
37
|
+
BAKE_TMPFS = ["/run"]
|
|
38
|
+
|
|
39
|
+
_ARCH_PLATFORM = {"x86_64": "linux/amd64", "amd64": "linux/amd64", "arm64": "linux/arm64", "aarch64": "linux/arm64"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def bake_container_name(image_name: str, runtime: str) -> str:
|
|
43
|
+
"""The build container's name: what ``ansible_host`` names for the
|
|
44
|
+
docker connection, so one per image and runtime."""
|
|
45
|
+
return tag_name(f"csis-bake-{runtime}-{image_name}").lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _label_change(key: str, value: str) -> str:
|
|
49
|
+
v = str(value).replace("\\", "\\\\").replace('"', '\\"')
|
|
50
|
+
return f'LABEL {key}="{v}"'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def docker_source(model: Any, image: Any, *, runtime: str, source_type: str, subconfig: Any,
|
|
54
|
+
self_subconfig: Any, psi: Any, tags: dict[str, str], final_name: str,
|
|
55
|
+
pinned: str | None) -> list[str]:
|
|
56
|
+
c: dict[str, Any] = {}
|
|
57
|
+
# --- the bake's parent image
|
|
58
|
+
if pinned:
|
|
59
|
+
c["image"] = pinned if is_image_id(pinned) else str(pinned)
|
|
60
|
+
elif psi.is_resolved():
|
|
61
|
+
c["image"] = str(psi.identifier)
|
|
62
|
+
else:
|
|
63
|
+
# deferred: the parent series' floating tag, moved by every bake of
|
|
64
|
+
# the series (the daemon's own "newest of the family")
|
|
65
|
+
tag = psi.floating_tag() if hasattr(psi, "floating_tag") else None
|
|
66
|
+
if not tag:
|
|
67
|
+
raise ValueError(f"image {image.get_name()} on runtime {runtime}: no floating tag for its deferred "
|
|
68
|
+
f"parent {psi.source_name!r}")
|
|
69
|
+
c["image"] = tag
|
|
70
|
+
# the query pulled every vendor image it resolved; our own builds are
|
|
71
|
+
# local and no registry has them
|
|
72
|
+
c["pull"] = False
|
|
73
|
+
c["commit"] = True
|
|
74
|
+
arch = getattr(image, "architecture", None) or getattr(psi, "architecture", None)
|
|
75
|
+
if arch and str(arch) not in OOPS_DEFAULTS and str(arch) in _ARCH_PLATFORM:
|
|
76
|
+
c["platform"] = _ARCH_PLATFORM[str(arch)]
|
|
77
|
+
# --- the build container: named (ansible reaches it by name), under an
|
|
78
|
+
# init, on the runtime's network, with its machine type's limits
|
|
79
|
+
run: list[str] = ["-d", "-i", "-t", "--init", "--name", bake_container_name(image.get_name(), runtime)]
|
|
80
|
+
networking = getattr(model, "networking", None)
|
|
81
|
+
network = getattr(networking, "network", None) if networking is not None else None
|
|
82
|
+
if network and network not in OOPS_DEFAULTS:
|
|
83
|
+
run += ["--network", str(network)]
|
|
84
|
+
mt_name = None
|
|
85
|
+
for cand in (getattr(subconfig, "machine_type", None) if subconfig else None,
|
|
86
|
+
getattr(subconfig, "default_machine_type", None) if subconfig else None,
|
|
87
|
+
model.get_default_machine_type()):
|
|
88
|
+
if cand and cand not in OOPS_DEFAULTS:
|
|
89
|
+
mt_name = str(cand)
|
|
90
|
+
break
|
|
91
|
+
mt = (model.get_machine_types() or {}).get(mt_name) if mt_name else None
|
|
92
|
+
if mt is not None:
|
|
93
|
+
if getattr(mt, "cpus", None):
|
|
94
|
+
run += ["--cpus", str(mt.cpus)]
|
|
95
|
+
if getattr(mt, "memory", None):
|
|
96
|
+
run += ["--memory", str(mt.memory)]
|
|
97
|
+
# the shell the bake provisions through is the CONTAINER's command, not an
|
|
98
|
+
# entrypoint: packer's default `--entrypoint=/bin/sh` becomes the committed
|
|
99
|
+
# image's entrypoint, and `ENTRYPOINT []` in `changes` does not clear it
|
|
100
|
+
# (found live: every later `docker run <image> <cmd>` ran `/bin/sh <cmd>`)
|
|
101
|
+
run += ["--", "{{.Image}}", "/bin/sh"]
|
|
102
|
+
c["run_command"] = [f'"{a}"' for a in run]
|
|
103
|
+
c["tmpfs"] = [f'"{t}"' for t in BAKE_TMPFS]
|
|
104
|
+
# --- what the committed image carries
|
|
105
|
+
changes = [_label_change(k, v) for k, v in sorted(tags.items())]
|
|
106
|
+
changes.append("CMD " + "[" + ", ".join(f'"{a}"' for a in KEEPALIVE_CMD) + "]")
|
|
107
|
+
c["changes"] = [f'"{ch.replace(chr(92), chr(92) * 2).replace(chr(34), chr(92) + chr(34))}"' for ch in changes]
|
|
108
|
+
for k, v in c.items():
|
|
109
|
+
if isinstance(v, str):
|
|
110
|
+
c[k] = f'"{v}"'
|
|
111
|
+
doc = Builder()
|
|
112
|
+
doc.block("source", labels=[f'"{source_type}"', f'"{image.get_name()}"'], **c)
|
|
113
|
+
return hcl2.dumps(doc.build(), formatter_options=FO).splitlines()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def docker_tag_post_processor(image: Any, *, repository: str, source_type: str, final_name: str,
|
|
117
|
+
prefix: str | None) -> list[str]:
|
|
118
|
+
"""The ``docker-tag`` post-processor of one image's build: the final name
|
|
119
|
+
and the series' floating tag, in the runtime's repository."""
|
|
120
|
+
names = [tag_name(final_name)] if final_name else []
|
|
121
|
+
if prefix and tag_name(prefix) not in names:
|
|
122
|
+
names.append(tag_name(prefix))
|
|
123
|
+
if not names:
|
|
124
|
+
return []
|
|
125
|
+
return [
|
|
126
|
+
f" # stage 103: the build's names are tags ({image.get_name()}); the floating tag is the series' newest",
|
|
127
|
+
' post-processor "docker-tag" {',
|
|
128
|
+
f' only = ["{source_type}.{image.get_name()}"]',
|
|
129
|
+
f' repository = "{repository}"',
|
|
130
|
+
" tags = [" + ", ".join(f'"{n}"' for n in names) + "]",
|
|
131
|
+
" }",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def quoted(parts: list[str]) -> str:
|
|
136
|
+
return " ".join(shlex.quote(p) for p in parts)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = ["DOCKER_SOURCE", "KEEPALIVE_CMD", "SELF", "bake_container_name", "docker_source",
|
|
140
|
+
"docker_tag_post_processor"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cs_image_system.base.models.provider_specific_image import ProviderSpecificImage
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DockerProviderSpecificImage(ProviderSpecificImage):
|
|
14
|
+
"""An image as a container daemon knows it. Resolved: a vendor image by
|
|
15
|
+
digest (``<repository>@sha256:...``, what the query recorded) or one of
|
|
16
|
+
our own builds by image id (``sha256:...``). Deferred: the series'
|
|
17
|
+
floating tag ``<repository>:<prefix>``, which every bake of the series
|
|
18
|
+
moves to its newest build -- the daemon has no "newest image matching a
|
|
19
|
+
name pattern" query, so the tag IS that query."""
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def csis_name(cls) -> str:
|
|
23
|
+
return "docker"
|
|
24
|
+
|
|
25
|
+
def floating_tag(self) -> str | None:
|
|
26
|
+
"""``<repository>:<prefix as a tag>`` for a deferred image."""
|
|
27
|
+
if not self.deferred_name_pattern:
|
|
28
|
+
return None
|
|
29
|
+
from cs_image_system.base.global_context import GlobalTypeContext
|
|
30
|
+
from .docker_names import tag_name
|
|
31
|
+
rtb = GlobalTypeContext().runtime_builders.get(self.runtime)
|
|
32
|
+
model = getattr(rtb, "model", None)
|
|
33
|
+
repo = model.get_repository() if model is not None and hasattr(model, "get_repository") else "csis"
|
|
34
|
+
return f"{repo}:{tag_name(self.deferred_name_pattern)}"
|
|
35
|
+
|
|
36
|
+
def get_query_assets(self) -> dict[str, Any] | None:
|
|
37
|
+
if self.is_resolved():
|
|
38
|
+
return {"image": str(self.identifier)}
|
|
39
|
+
tag = self.floating_tag()
|
|
40
|
+
return {"image": tag} if tag else None
|