boatyard 0.6.0.dev170__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.
- boatyard/__init__.py +0 -0
- boatyard/build.py +267 -0
- boatyard/cli.py +23 -0
- boatyard/compose.py +24 -0
- boatyard/config/__init__.py +0 -0
- boatyard/config/config.py +787 -0
- boatyard/config/lock.py +152 -0
- boatyard/config/values.py +188 -0
- boatyard/crane.py +23 -0
- boatyard/dockerfiles.py +201 -0
- boatyard/lock.py +233 -0
- boatyard/markers.py +201 -0
- boatyard/paths.py +5 -0
- boatyard/publish.py +72 -0
- boatyard/py.typed +0 -0
- boatyard/score.py +267 -0
- boatyard/score_compose.py +274 -0
- boatyard/score_k8s.py +152 -0
- boatyard/score_patch_templates/gpu-cuda.tpl +24 -0
- boatyard/score_patch_templates/gpu-pod.tpl +43 -0
- boatyard/score_patch_templates/gpu-rocm.tpl +30 -0
- boatyard/score_patch_templates/probe-adapter-healthchecks.tpl +55 -0
- boatyard/score_patch_templates/restart.tpl +21 -0
- boatyard/score_patch_templates/scale-to-zero.tpl +30 -0
- boatyard/temp.py +5 -0
- boatyard/verbs/__init__.py +0 -0
- boatyard/verbs/build.py +54 -0
- boatyard/verbs/down.py +39 -0
- boatyard/verbs/lock.py +34 -0
- boatyard/verbs/matrix.py +57 -0
- boatyard/verbs/mirror.py +50 -0
- boatyard/verbs/publish.py +138 -0
- boatyard/verbs/score.py +161 -0
- boatyard/verbs/tree_tags.py +35 -0
- boatyard/verbs/up.py +265 -0
- boatyard/yaml.py +34 -0
- boatyard-0.6.0.dev170.dist-info/METADATA +12 -0
- boatyard-0.6.0.dev170.dist-info/RECORD +42 -0
- boatyard-0.6.0.dev170.dist-info/WHEEL +4 -0
- boatyard-0.6.0.dev170.dist-info/entry_points.txt +2 -0
- boatyard-0.6.0.dev170.dist-info/licenses/LICENSE +201 -0
- boatyard-0.6.0.dev170.dist-info/licenses/NOTICE +1 -0
boatyard/verbs/score.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from tempfile import TemporaryDirectory
|
|
6
|
+
from typing import Annotated, NamedTuple
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from bashrun.bash import bash, bash_check, bash_output
|
|
10
|
+
|
|
11
|
+
from boatyard import dockerfiles
|
|
12
|
+
from boatyard.config.config import ConfigFile, Target
|
|
13
|
+
from boatyard.config.lock import LOCK_PATH, LockFile
|
|
14
|
+
from boatyard.yaml import load_yaml_model
|
|
15
|
+
from boatyard.score import (
|
|
16
|
+
SECRETS_FILE,
|
|
17
|
+
SECRETS_OUTPUT,
|
|
18
|
+
SecretsDeclaration,
|
|
19
|
+
write_file,
|
|
20
|
+
)
|
|
21
|
+
from boatyard.score_compose import generate_compose
|
|
22
|
+
from boatyard.paths import DEFAULT_CONFIG_PATH
|
|
23
|
+
from boatyard.score_k8s import generate_k8s
|
|
24
|
+
|
|
25
|
+
app = typer.Typer()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolRequirement(NamedTuple):
|
|
29
|
+
binary: str
|
|
30
|
+
version: str
|
|
31
|
+
probe: str
|
|
32
|
+
archive: str
|
|
33
|
+
extract: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def generate_score(
|
|
38
|
+
target: Annotated[Target, typer.Option(help="Which Score artifact to generate.")] = "both",
|
|
39
|
+
local: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--local",
|
|
43
|
+
help="Generate for a local cluster: local storage class, written to the configured local output path.",
|
|
44
|
+
),
|
|
45
|
+
] = False,
|
|
46
|
+
check: Annotated[
|
|
47
|
+
bool,
|
|
48
|
+
typer.Option("--check", help="Regenerate and exit 1 when the committed artifacts are stale."),
|
|
49
|
+
] = False,
|
|
50
|
+
) -> None:
|
|
51
|
+
if check and local:
|
|
52
|
+
raise typer.BadParameter("--check and --local are mutually exclusive")
|
|
53
|
+
root = Path.cwd()
|
|
54
|
+
config_path = root / DEFAULT_CONFIG_PATH
|
|
55
|
+
config = ConfigFile.load(config_path) if config_path.is_file() else None
|
|
56
|
+
if config is None or not (config.local_service_entries() or config.k8s):
|
|
57
|
+
if check:
|
|
58
|
+
print("no local service entries and no k8s section; nothing to check")
|
|
59
|
+
return
|
|
60
|
+
raise ValueError("boatyard.yml declares no local service entries and no k8s section")
|
|
61
|
+
local_entries = config.local_service_entries()
|
|
62
|
+
|
|
63
|
+
if config.k8s is None and target in ("k8s", "both"):
|
|
64
|
+
message = (
|
|
65
|
+
"--target k8s: this config declares no k8s section (compose-only)"
|
|
66
|
+
if target == "k8s"
|
|
67
|
+
else "--target both: this config declares no k8s section; pass --target compose"
|
|
68
|
+
)
|
|
69
|
+
raise typer.BadParameter(message)
|
|
70
|
+
if not local_entries and target in ("compose", "both"):
|
|
71
|
+
message = (
|
|
72
|
+
"--target compose: this config declares no local service entries (k8s-only)"
|
|
73
|
+
if target == "compose"
|
|
74
|
+
else "--target both: this config declares no local service entries; pass --target k8s"
|
|
75
|
+
)
|
|
76
|
+
raise typer.BadParameter(message)
|
|
77
|
+
if config.k8s is None and local:
|
|
78
|
+
raise typer.BadParameter("--local requires a k8s section; this config declares none")
|
|
79
|
+
|
|
80
|
+
# Require the build keys, derive the tree tags, and load the lock.
|
|
81
|
+
if not (config.repository or config.base_images):
|
|
82
|
+
raise ValueError("repository or base_images is required in boatyard.yml")
|
|
83
|
+
service_shas = dockerfiles.compute_service_shas(root, config.service_builds)
|
|
84
|
+
lock_path = root / LOCK_PATH
|
|
85
|
+
if not lock_path.is_file():
|
|
86
|
+
raise ValueError(f"{lock_path.as_posix()}: the image lock is required")
|
|
87
|
+
lock = LockFile.load(lock_path, config)
|
|
88
|
+
if not lock.images and not lock.charts:
|
|
89
|
+
raise ValueError(f"{lock_path.as_posix()}: the image lock carries no entries")
|
|
90
|
+
|
|
91
|
+
# Require the secrets declaration; an empty file parses as the empty declaration.
|
|
92
|
+
secrets_path = root / SECRETS_FILE
|
|
93
|
+
if not secrets_path.is_file():
|
|
94
|
+
raise ValueError(f"{secrets_path.as_posix()}: the secrets declaration is required")
|
|
95
|
+
secrets = load_yaml_model(SecretsDeclaration, secrets_path)
|
|
96
|
+
|
|
97
|
+
ensure_tools(
|
|
98
|
+
[
|
|
99
|
+
ToolRequirement(
|
|
100
|
+
binary=binary,
|
|
101
|
+
version=version,
|
|
102
|
+
probe=f"{binary} {version}",
|
|
103
|
+
archive=(
|
|
104
|
+
f"https://github.com/score-spec/{binary}/releases/download/{version}/"
|
|
105
|
+
f"{binary}_{version}_linux_amd64.tar.gz"
|
|
106
|
+
),
|
|
107
|
+
extract=binary,
|
|
108
|
+
)
|
|
109
|
+
for binary, version in (
|
|
110
|
+
("score-compose", config.score_compose_version),
|
|
111
|
+
("score-k8s", config.score_k8s_version),
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
+ [
|
|
115
|
+
ToolRequirement(
|
|
116
|
+
binary="helm",
|
|
117
|
+
version=config.helm_version,
|
|
118
|
+
probe=f'"v{config.helm_version}"',
|
|
119
|
+
archive=f"https://get.helm.sh/helm-v{config.helm_version}-linux-amd64.tar.gz",
|
|
120
|
+
extract="--strip-components=1 linux-amd64/helm",
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
outputs: list[Path] = []
|
|
126
|
+
if target in ("both", "compose"):
|
|
127
|
+
outputs.extend(generate_compose(config, lock, service_shas, secrets))
|
|
128
|
+
|
|
129
|
+
if target in ("both", "k8s") and config.k8s is not None:
|
|
130
|
+
outputs.append(generate_k8s(config, config.k8s, lock, service_shas, local, secrets))
|
|
131
|
+
|
|
132
|
+
secrets_output = root / SECRETS_OUTPUT
|
|
133
|
+
secrets_output.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
write_file(
|
|
135
|
+
secrets_output,
|
|
136
|
+
"# Generated by generate-score from the secrets declaration\n"
|
|
137
|
+
+ "".join(f"{name}\n" for name, key in sorted(secrets.root.items()) if key.mint),
|
|
138
|
+
)
|
|
139
|
+
outputs.append(secrets_output)
|
|
140
|
+
|
|
141
|
+
if check:
|
|
142
|
+
pathspec = " ".join(output.as_posix() for output in outputs)
|
|
143
|
+
if bash_output(f"git status --porcelain -- {pathspec}").strip():
|
|
144
|
+
bash(f"git diff -- {pathspec}")
|
|
145
|
+
raise SystemExit(
|
|
146
|
+
"score artifacts are stale. Run 'generate-score' and commit the result as 'Run generate-score'."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ensure_tools(requirements: list[ToolRequirement]) -> None:
|
|
151
|
+
for requirement in requirements:
|
|
152
|
+
if bash_check(f"{requirement.binary} version") and requirement.probe in bash_output(
|
|
153
|
+
f"{requirement.binary} version"
|
|
154
|
+
):
|
|
155
|
+
continue
|
|
156
|
+
print(f"Install {requirement.binary} {requirement.version}")
|
|
157
|
+
sudo = "sudo " if os.geteuid() != 0 else ""
|
|
158
|
+
with TemporaryDirectory() as directory:
|
|
159
|
+
archive = Path(directory) / Path(requirement.archive).name
|
|
160
|
+
bash(f"curl -fsSL {requirement.archive} -o {archive.as_posix()}")
|
|
161
|
+
bash(f"{sudo}tar -xzf {archive.as_posix()} -C /usr/local/bin {requirement.extract}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from boatyard import dockerfiles
|
|
9
|
+
from boatyard.config.config import ConfigFile
|
|
10
|
+
from boatyard.dockerfiles import service_sha_env
|
|
11
|
+
from boatyard.paths import DEFAULT_CONFIG_PATH
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command()
|
|
17
|
+
def tree_tags(
|
|
18
|
+
services: Annotated[
|
|
19
|
+
list[str] | None,
|
|
20
|
+
typer.Argument(help="Derived service names; each answers its HEAD tree tag, one per line."),
|
|
21
|
+
] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
root = Path.cwd()
|
|
24
|
+
config = ConfigFile.load(root / DEFAULT_CONFIG_PATH)
|
|
25
|
+
vocabulary = sorted(config.service_builds)
|
|
26
|
+
if not services:
|
|
27
|
+
raise typer.BadParameter(
|
|
28
|
+
f"at least one service name is required; this repo derives {vocabulary or 'no services'}"
|
|
29
|
+
)
|
|
30
|
+
unknown = [name for name in services if name not in config.service_builds]
|
|
31
|
+
if unknown:
|
|
32
|
+
raise typer.BadParameter(f"unknown service(s) {unknown}; this repo derives {vocabulary}")
|
|
33
|
+
shas = dockerfiles.compute_service_shas(config.root, config.service_builds)
|
|
34
|
+
for name in services:
|
|
35
|
+
print(shas[service_sha_env(name)])
|
boatyard/verbs/up.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Generator, Mapping
|
|
5
|
+
from contextlib import ExitStack, contextmanager
|
|
6
|
+
from itertools import groupby
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tempfile import NamedTemporaryFile
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
import yaml
|
|
13
|
+
from bashrun.bash import bash_handoff
|
|
14
|
+
|
|
15
|
+
from boatyard.build import run_build
|
|
16
|
+
from boatyard.compose import compose_command
|
|
17
|
+
from boatyard.config.config import (
|
|
18
|
+
CADDY_IMAGE,
|
|
19
|
+
ConfigFile,
|
|
20
|
+
EDGE_SERVICE_NAME,
|
|
21
|
+
OCI_REFERENCE_PREFIX,
|
|
22
|
+
Yaml,
|
|
23
|
+
bundle_lock_key,
|
|
24
|
+
compose_bundle_output_path,
|
|
25
|
+
double_dollars,
|
|
26
|
+
env_lock_slot,
|
|
27
|
+
scan_bundle_texts,
|
|
28
|
+
)
|
|
29
|
+
from boatyard.config.lock import LockFile
|
|
30
|
+
from boatyard.config.values import ValuesFile
|
|
31
|
+
from boatyard.paths import DEFAULT_CONFIG_PATH, LOCK_PATH, VALUES_FILE
|
|
32
|
+
|
|
33
|
+
# Name the edge listener port and its configs key
|
|
34
|
+
EDGE_LISTEN_PORT = 8443
|
|
35
|
+
EDGE_CONFIG_NAME = "devkit-edge-caddyfile"
|
|
36
|
+
|
|
37
|
+
app = typer.Typer()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def up(
|
|
42
|
+
attached: Annotated[bool, typer.Option("--attached", "-a", help="Run in foreground (not detached)")] = False,
|
|
43
|
+
quiet_pull: Annotated[
|
|
44
|
+
bool,
|
|
45
|
+
typer.Option("--quiet-pull", "-q", help="Suppress per-layer pull progress (still shows pull/push totals)."),
|
|
46
|
+
] = False,
|
|
47
|
+
build: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
typer.Option("--build", help="Build all images locally before bringing the stack up"),
|
|
50
|
+
] = False,
|
|
51
|
+
) -> None:
|
|
52
|
+
# Load the config and the typed machine values from the working directory
|
|
53
|
+
root = Path.cwd()
|
|
54
|
+
config = ConfigFile.load(root / DEFAULT_CONFIG_PATH)
|
|
55
|
+
if not (root / VALUES_FILE).exists():
|
|
56
|
+
raise ValueError(f"{VALUES_FILE.as_posix()} is required")
|
|
57
|
+
config_values = ValuesFile.load(root / VALUES_FILE, config)
|
|
58
|
+
|
|
59
|
+
# Require the config to declare a compose stack
|
|
60
|
+
if not config.services:
|
|
61
|
+
raise ValueError("boatyard.yml must declare services")
|
|
62
|
+
|
|
63
|
+
# Resolve the chosen options into leaf services
|
|
64
|
+
services = config.resolve_services(config_values.choices)
|
|
65
|
+
|
|
66
|
+
# Project the consumer lock's manifest references and seed the assembly accumulators
|
|
67
|
+
lock = LockFile.load(config.root / LOCK_PATH, config)
|
|
68
|
+
consumer_entries = lock.manifest_references(config_values.platform)
|
|
69
|
+
entries: dict[str, str] = {}
|
|
70
|
+
includes: list[Yaml] = []
|
|
71
|
+
requirements: dict[str, list[str]] = {}
|
|
72
|
+
|
|
73
|
+
# Collect each entry's include, requirements, and env entries in declaration order
|
|
74
|
+
for service in services:
|
|
75
|
+
name = service.name
|
|
76
|
+
|
|
77
|
+
# Include a remote ref as its lock-pinned digest
|
|
78
|
+
if service.entry.ref is not None and name not in config_values.path_overrides:
|
|
79
|
+
key = bundle_lock_key(name)
|
|
80
|
+
pinned = lock.require(key, f"bundle {name!r}")
|
|
81
|
+
requirements[name] = lock.bundles[key].requires
|
|
82
|
+
includes.append(f"{OCI_REFERENCE_PREFIX}{pinned}")
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Include a local entry through its generated output
|
|
86
|
+
if service.entry.ref is None:
|
|
87
|
+
bundle_compose_path = compose_bundle_output_path(config.root, name)
|
|
88
|
+
if not bundle_compose_path.is_file():
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"selected entry {name!r} has no bundle output {bundle_compose_path.as_posix()}; generate-score writes it"
|
|
91
|
+
)
|
|
92
|
+
lock_references = consumer_entries
|
|
93
|
+
else:
|
|
94
|
+
# Include an overridden ref through its local path
|
|
95
|
+
candidate = config_values.path_overrides[name]
|
|
96
|
+
bundle_compose_path = (candidate if candidate.is_absolute() else config.root / candidate).resolve()
|
|
97
|
+
if not bundle_compose_path.is_file():
|
|
98
|
+
raise ValueError(f"path_overrides.{name}: {bundle_compose_path.as_posix()} does not exist")
|
|
99
|
+
|
|
100
|
+
# Walk up from the override to the boatyard.yml-bearing producer root
|
|
101
|
+
producer_root = next(
|
|
102
|
+
(ancestor for ancestor in bundle_compose_path.parents if (ancestor / DEFAULT_CONFIG_PATH).is_file()),
|
|
103
|
+
None,
|
|
104
|
+
)
|
|
105
|
+
if producer_root is None:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"{bundle_compose_path.as_posix()}: no boatyard.yml-bearing repo root above the bundle override"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Project the producer lock's manifest references under the consumer's platform
|
|
111
|
+
lock_references = LockFile.load(
|
|
112
|
+
producer_root / LOCK_PATH, ConfigFile.load(producer_root / DEFAULT_CONFIG_PATH)
|
|
113
|
+
).manifest_references(config_values.platform)
|
|
114
|
+
|
|
115
|
+
# Scan the bundle file for its requirements and refuse the reserved edge service
|
|
116
|
+
text = bundle_compose_path.read_text(encoding="utf-8")
|
|
117
|
+
scan = scan_bundle_texts(name, service.routes, service.ports, [text])
|
|
118
|
+
if EDGE_SERVICE_NAME in scan.service_names:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"{bundle_compose_path.as_posix()}: "
|
|
121
|
+
f"the included bundle defines the reserved service {EDGE_SERVICE_NAME!r}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Record the entry's requirements, include the file by absolute path, and fan its lock keys under the marker
|
|
125
|
+
requirements[name] = scan.requires
|
|
126
|
+
includes.append(bundle_compose_path.resolve().as_posix())
|
|
127
|
+
entries.update({env_lock_slot(name, key): value for key, value in lock_references.items()})
|
|
128
|
+
|
|
129
|
+
# Refuse bundles whose requirements no environment source fills, naming shell-set slots as ignored
|
|
130
|
+
filled = {variable for variable, value in {**config_values.variables, **entries}.items() if value}
|
|
131
|
+
missing = {
|
|
132
|
+
name: sorted(set(required) - filled) for name, required in requirements.items() if set(required) - filled
|
|
133
|
+
}
|
|
134
|
+
if missing:
|
|
135
|
+
name, absent = min(missing.items())
|
|
136
|
+
ignored = sorted(shell_name for shell_name in absent if os.environ.get(shell_name))
|
|
137
|
+
note = f"; {', '.join(ignored)} set in the shell and ignored" if ignored else ""
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"bundle {name!r} requires {absent}, which no environment source sets "
|
|
140
|
+
f"(boatyard.values.yaml or the lock env file){note}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Group the ports under their namespaced owners into per-service stanzas
|
|
144
|
+
document_services: dict[str, Yaml] = {
|
|
145
|
+
f"{service.name}-{owner}": {
|
|
146
|
+
"ports": [port.model_dump(exclude={"service"}, exclude_none=True) for port in owner_ports]
|
|
147
|
+
}
|
|
148
|
+
for service in services
|
|
149
|
+
for owner, owner_ports in groupby(
|
|
150
|
+
sorted(service.ports, key=lambda port: port.service), key=lambda port: port.service
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Assemble the root document, sections omitted when empty
|
|
155
|
+
document: dict[str, Yaml] = {"name": config.project_name}
|
|
156
|
+
|
|
157
|
+
# Render the edge and its Caddyfile when a gateway is declared
|
|
158
|
+
if config.gateway is not None:
|
|
159
|
+
edge: dict[str, Yaml] = {
|
|
160
|
+
"image": consumer_entries[CADDY_IMAGE],
|
|
161
|
+
"configs": [{"source": EDGE_CONFIG_NAME, "target": "/etc/caddy/Caddyfile"}],
|
|
162
|
+
"ports": [{"target": str(EDGE_LISTEN_PORT), "published": config.gateway, "protocol": "tcp"}],
|
|
163
|
+
# Attach the edge to every selected bundle's network
|
|
164
|
+
"networks": dict.fromkeys(f"{service.name}-net" for service in services),
|
|
165
|
+
}
|
|
166
|
+
if any(service.auth for service in services):
|
|
167
|
+
if config_values.public_url is None:
|
|
168
|
+
raise ValueError("an auth option is selected; public_url is required in boatyard.values.yaml")
|
|
169
|
+
edge["environment"] = config_values.public_url.forwarded
|
|
170
|
+
document_services[EDGE_SERVICE_NAME] = edge
|
|
171
|
+
|
|
172
|
+
# Render one handle block per route in declaration order
|
|
173
|
+
lines = [f":{EDGE_LISTEN_PORT} {{"]
|
|
174
|
+
for service, index, route in (
|
|
175
|
+
(service, index, route) for service in services for index, route in enumerate(service.routes)
|
|
176
|
+
):
|
|
177
|
+
upstream = f"{service.name}-{route.upstream}"
|
|
178
|
+
opener = (
|
|
179
|
+
("\thandle_path" if route.strip_prefix else "\thandle")
|
|
180
|
+
+ ("" if route.path == "/*" else f" {route.path}")
|
|
181
|
+
+ " {"
|
|
182
|
+
)
|
|
183
|
+
if route.raw is not None:
|
|
184
|
+
lines.extend((opener, *["\t\t" + line for line in route.raw.splitlines()], "\t}"))
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if service.auth and index == 0:
|
|
188
|
+
lines.extend((
|
|
189
|
+
opener,
|
|
190
|
+
*[
|
|
191
|
+
f"\t\treverse_proxy {upstream} {{",
|
|
192
|
+
"\t\t\theader_up X-Forwarded-Proto {$FORWARDED_PROTO}",
|
|
193
|
+
"\t\t\theader_up X-Forwarded-Port {$FORWARDED_PORT}",
|
|
194
|
+
"\t\t}",
|
|
195
|
+
],
|
|
196
|
+
"\t}",
|
|
197
|
+
))
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
lines.extend((opener, *[f"\t\treverse_proxy {upstream}"], "\t}"))
|
|
201
|
+
lines.append("}")
|
|
202
|
+
document["configs"] = {EDGE_CONFIG_NAME: {"content": double_dollars("\n".join(lines) + "\n")}}
|
|
203
|
+
|
|
204
|
+
if includes:
|
|
205
|
+
document["include"] = includes
|
|
206
|
+
if document_services:
|
|
207
|
+
document["services"] = document_services
|
|
208
|
+
|
|
209
|
+
with ExitStack() as stack:
|
|
210
|
+
# Materialize the env file
|
|
211
|
+
with NamedTemporaryFile("w", suffix=".env", prefix="devkit-env-", delete=False, encoding="utf-8") as handle:
|
|
212
|
+
handle.write(
|
|
213
|
+
"".join(
|
|
214
|
+
f"# {header}\n{render_env_entries(section)}"
|
|
215
|
+
for header, section in ((VALUES_FILE.name, config_values.variables), (LOCK_PATH.name, entries))
|
|
216
|
+
if section
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
materialized_env_file = Path(handle.name)
|
|
220
|
+
stack.enter_context(lock_env_unwind(materialized_env_file))
|
|
221
|
+
|
|
222
|
+
# Materialize the compose file
|
|
223
|
+
with NamedTemporaryFile(
|
|
224
|
+
"w", suffix=".yaml", prefix="devkit-assembly-", delete=False, encoding="utf-8"
|
|
225
|
+
) as handle:
|
|
226
|
+
handle.write(yaml.safe_dump(document, sort_keys=False, default_flow_style=False))
|
|
227
|
+
materialized_compose_file = Path(handle.name)
|
|
228
|
+
stack.callback(materialized_compose_file.unlink, missing_ok=True)
|
|
229
|
+
|
|
230
|
+
# Build local images
|
|
231
|
+
if build:
|
|
232
|
+
run_build(config=config)
|
|
233
|
+
|
|
234
|
+
# Hand off to docker compose
|
|
235
|
+
bash_handoff(
|
|
236
|
+
compose_command(
|
|
237
|
+
root,
|
|
238
|
+
materialized_compose_file,
|
|
239
|
+
"up",
|
|
240
|
+
*["--pull", "missing", *(["--quiet-pull"] if quiet_pull else []), *(["-d"] if not attached else [])],
|
|
241
|
+
env_file=materialized_env_file,
|
|
242
|
+
slot_names=[
|
|
243
|
+
*config_values.variables,
|
|
244
|
+
*entries,
|
|
245
|
+
*(name for required in requirements.values() for name in required),
|
|
246
|
+
],
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def render_env_entries(entries: Mapping[str, str]) -> str:
|
|
252
|
+
# Render the entries as sorted dotenv lines
|
|
253
|
+
return "".join(f"{name}={reference}\n" for name, reference in sorted(entries.items()))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@contextmanager
|
|
257
|
+
def lock_env_unwind(path: Path) -> Generator[None]:
|
|
258
|
+
# Keep the env file on failure echoing only its path, since contents may carry secrets, and unlink it on success
|
|
259
|
+
try:
|
|
260
|
+
yield
|
|
261
|
+
except BaseException:
|
|
262
|
+
print(f"env file kept for inspection: {path}")
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
path.unlink(missing_ok=True)
|
boatyard/yaml.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_yaml_model[ModelT: BaseModel](
|
|
10
|
+
model: type[ModelT], path: Path, context: dict[str, object] | None = None
|
|
11
|
+
) -> ModelT:
|
|
12
|
+
source = path.as_posix()
|
|
13
|
+
text = path.read_text(encoding="utf-8")
|
|
14
|
+
document_node = yaml.SafeLoader(text).get_single_node()
|
|
15
|
+
if document_node is not None:
|
|
16
|
+
_reject_duplicate_keys(document_node, source)
|
|
17
|
+
loaded: object = yaml.safe_load(text)
|
|
18
|
+
if loaded is None:
|
|
19
|
+
loaded = {}
|
|
20
|
+
return model.model_validate(loaded, context=context)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _reject_duplicate_keys(node: yaml.Node, source: str) -> None:
|
|
24
|
+
if isinstance(node, yaml.MappingNode):
|
|
25
|
+
seen: set[object] = set()
|
|
26
|
+
for key_node, value_node in node.value:
|
|
27
|
+
key = key_node.value if isinstance(key_node, yaml.ScalarNode) else repr(key_node.value)
|
|
28
|
+
if key in seen:
|
|
29
|
+
raise ValueError(f"{source}: line {key_node.start_mark.line + 1}: duplicate key {key!r}")
|
|
30
|
+
seen.add(key)
|
|
31
|
+
_reject_duplicate_keys(value_node, source)
|
|
32
|
+
elif isinstance(node, yaml.SequenceNode):
|
|
33
|
+
for item in node.value:
|
|
34
|
+
_reject_duplicate_keys(item, source)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: boatyard
|
|
3
|
+
Version: 0.6.0.dev170
|
|
4
|
+
Summary: Deploy-files definition language and compiler, plus a runtime for the compose target
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
License-File: NOTICE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: bashrun>=0.1.0
|
|
9
|
+
Requires-Dist: packaging>=24.1
|
|
10
|
+
Requires-Dist: pydantic>=2.11.7
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Requires-Dist: typer>=0.17.4
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
boatyard/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
boatyard/build.py,sha256=Ys8UmebsLzfOnUpfpuvCSeIBX1A1DOcnNxkf6QyntE8,10666
|
|
3
|
+
boatyard/cli.py,sha256=i6cyJpHXIb3_9O1i2yobhS1LaE1byeSPs91ZWbC-ZGE,720
|
|
4
|
+
boatyard/compose.py,sha256=XL29GyVqMVsfNcgirVzwqUCyVwoiPx59aGBu6fUSgx4,939
|
|
5
|
+
boatyard/crane.py,sha256=hcIMCE5CLQKFgpyySVJubuuJuFJ6MST30ws3LWuSEfo,627
|
|
6
|
+
boatyard/dockerfiles.py,sha256=PIRmxSCkgWBRYz7wHTE2XnzsXLa9mgSd-c2jTiQU63w,8706
|
|
7
|
+
boatyard/lock.py,sha256=rCbiGlZv8gsD3nRqqapfzQB7Lemff-pjaXnRGiyBMDY,9464
|
|
8
|
+
boatyard/markers.py,sha256=fTTEx33phwxZxZecLhkQkWTcJgLJ0h6ZZQl6DSqjtvk,7393
|
|
9
|
+
boatyard/paths.py,sha256=gpR3ZhyciIFQQQGlvCPCxS8SFfVbSWrSw7gMiKiXUqs,150
|
|
10
|
+
boatyard/publish.py,sha256=XwQubwpTPDcAMgCxLjg5RCrIsWIWxmrEmzyBZZOuOpQ,2667
|
|
11
|
+
boatyard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
boatyard/score.py,sha256=NC1Oi4NMGU-85iHt7h8PYdze4DZTZyMex6MG7sANdus,9641
|
|
13
|
+
boatyard/score_compose.py,sha256=K4QD6VlEIXqGqmRZq8VpME0aYRR2RxQszVsQBSlJvkM,10011
|
|
14
|
+
boatyard/score_k8s.py,sha256=ZsSC5qukYY0FcLfjqhiVGCr1bFbUqQYGaPuIYCQeqCo,5040
|
|
15
|
+
boatyard/temp.py,sha256=fV1RYL7oamnoAsmDEx0yx4i4hIjLykT7hCuiiNL_7XA,131
|
|
16
|
+
boatyard/yaml.py,sha256=xoGfpaHPHxjSaMEkoy3BYxX2sRhhmVvM6SPh2zIP5cg,1236
|
|
17
|
+
boatyard/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
boatyard/config/config.py,sha256=pexCK7WNXL6JXn5Tc_WEZ-y6AqaB7FLVwkRon_7mATg,30937
|
|
19
|
+
boatyard/config/lock.py,sha256=qyjMvb6QgM5PSlYVg44oDNt195p52oY7GsPGllbOksk,6805
|
|
20
|
+
boatyard/config/values.py,sha256=FWHeXFENgJaO565qzffZw4k9USAgRc8ditYPeveBNNg,7668
|
|
21
|
+
boatyard/score_patch_templates/gpu-cuda.tpl,sha256=kJFpV0mzeW48cAr3hw97n6EmpW9w7SETAyTdhPO5YDQ,1032
|
|
22
|
+
boatyard/score_patch_templates/gpu-pod.tpl,sha256=c3plZTU6MrYKZXViFc0orXrFXS4QyQ7BuKesMkgWHic,1942
|
|
23
|
+
boatyard/score_patch_templates/gpu-rocm.tpl,sha256=7RdEapTwFPzZ-cuOLrXeTR50sHOZyI-_BAph5Mtag94,1148
|
|
24
|
+
boatyard/score_patch_templates/probe-adapter-healthchecks.tpl,sha256=B3dDUS1wWezwunxPpyfHlPwW2hkCBM-xyIOfqEVeXzM,2172
|
|
25
|
+
boatyard/score_patch_templates/restart.tpl,sha256=d_tAuZgw6Imwf8gpRWMqM4gjsZZ786VMAUZ9mQ-lvW8,909
|
|
26
|
+
boatyard/score_patch_templates/scale-to-zero.tpl,sha256=CFm08MWJJUw0BeSx_HkQKon3qJcz8DN0LZCMTIyA-X8,1120
|
|
27
|
+
boatyard/verbs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
boatyard/verbs/build.py,sha256=uFUXzV8wzOWUdH7YFHzdwpcjlqNDRyAyRMpcxW81jq0,2015
|
|
29
|
+
boatyard/verbs/down.py,sha256=qp-BthIEskhB1nvE_zFY5tF_He1-BOx1ts0BSY9FrpA,1267
|
|
30
|
+
boatyard/verbs/lock.py,sha256=zDb_vKaZr0WjYVMZbjwvKHUyuG3kUJ0TjyMyYL-Rdi8,1089
|
|
31
|
+
boatyard/verbs/matrix.py,sha256=Mli1TUAGkF5Y7vCD-Nla6Sq6kF91c-FcW-3O5RGCVx0,1956
|
|
32
|
+
boatyard/verbs/mirror.py,sha256=xCXM3ic9AuNRSkOwLTVCbvgfGT59KwPEXK4BQMeRcSY,1741
|
|
33
|
+
boatyard/verbs/publish.py,sha256=9m4jsD8KT_uliyLlotLOz3YWyYNq5OmUH-kgnlQaqro,5496
|
|
34
|
+
boatyard/verbs/score.py,sha256=APqmm9A7sut3ZqJ9AtjWGtLD33eItbiH5PM2Fose7fw,6294
|
|
35
|
+
boatyard/verbs/tree_tags.py,sha256=BN57sA42Z5g7aeAxVEsmGXegRt0idj9RUi96v1Dv7SA,1145
|
|
36
|
+
boatyard/verbs/up.py,sha256=17wUfxkL4ewZXxAIojnZavo4h4UraEh7Kbdv18WfBZE,11239
|
|
37
|
+
boatyard-0.6.0.dev170.dist-info/METADATA,sha256=RryIMz3tdmNnQJ2s9BuoqZBz4khkBUP1wmgs1G0aXkw,369
|
|
38
|
+
boatyard-0.6.0.dev170.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
39
|
+
boatyard-0.6.0.dev170.dist-info/entry_points.txt,sha256=vuiOwN-ufRd_fxnl3DIG_2V3kcv05l0kRXlVhFIPgNs,46
|
|
40
|
+
boatyard-0.6.0.dev170.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
|
|
41
|
+
boatyard-0.6.0.dev170.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
|
|
42
|
+
boatyard-0.6.0.dev170.dist-info/RECORD,,
|