offline-docker-cli 2.2.2__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.
- offline_docker/__init__.py +3 -0
- offline_docker/__main__.py +4 -0
- offline_docker/actions.py +173 -0
- offline_docker/build.py +245 -0
- offline_docker/cli.py +486 -0
- offline_docker/config.py +692 -0
- offline_docker/configure.py +282 -0
- offline_docker/context.py +111 -0
- offline_docker/defaults.py +48 -0
- offline_docker/discover.py +139 -0
- offline_docker/docker_cmd.py +136 -0
- offline_docker/download_debs.py +142 -0
- offline_docker/download_images.py +26 -0
- offline_docker/download_npm.py +106 -0
- offline_docker/download_trivy.py +90 -0
- offline_docker/download_wheels.py +363 -0
- offline_docker/download_yarn.py +208 -0
- offline_docker/ensure_base.py +61 -0
- offline_docker/hooks.py +84 -0
- offline_docker/init_cmd.py +54 -0
- offline_docker/load_images.py +34 -0
- offline_docker/oc_discovery.py +206 -0
- offline_docker/resolve.py +306 -0
- offline_docker/scan.py +496 -0
- offline_docker/sources/__init__.py +18 -0
- offline_docker/sources/base.py +18 -0
- offline_docker/sources/filesystem.py +17 -0
- offline_docker/sources/nexus.py +61 -0
- offline_docker/sources/nexus_client.py +67 -0
- offline_docker/sources/proget.py +49 -0
- offline_docker/sources/proget_client.py +48 -0
- offline_docker/sources/upstream.py +36 -0
- offline_docker/status.py +247 -0
- offline_docker/sync_cache.py +125 -0
- offline_docker/trivy_scan.py +86 -0
- offline_docker/tui/__init__.py +19 -0
- offline_docker/tui/app.py +56 -0
- offline_docker/tui/runner.py +87 -0
- offline_docker/tui/screens.py +333 -0
- offline_docker/user_config.py +89 -0
- offline_docker/utils.py +73 -0
- offline_docker/validate.py +53 -0
- offline_docker/wheel_prune.py +141 -0
- offline_docker_cli-2.2.2.dist-info/METADATA +417 -0
- offline_docker_cli-2.2.2.dist-info/RECORD +49 -0
- offline_docker_cli-2.2.2.dist-info/WHEEL +5 -0
- offline_docker_cli-2.2.2.dist-info/entry_points.txt +2 -0
- offline_docker_cli-2.2.2.dist-info/licenses/LICENSE +21 -0
- offline_docker_cli-2.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Thin wrappers around docker / docker compose CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_SUBPROCESS_ENCODING = "utf-8"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write_stdout(text: str) -> None:
|
|
16
|
+
try:
|
|
17
|
+
sys.stdout.write(text)
|
|
18
|
+
except UnicodeEncodeError:
|
|
19
|
+
sys.stdout.buffer.write(
|
|
20
|
+
text.encode(sys.stdout.encoding or _SUBPROCESS_ENCODING, errors="replace")
|
|
21
|
+
)
|
|
22
|
+
sys.stdout.flush()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CommandError(RuntimeError):
|
|
26
|
+
def __init__(self, cmd: list[str], returncode: int):
|
|
27
|
+
self.cmd = cmd
|
|
28
|
+
self.returncode = returncode
|
|
29
|
+
super().__init__(f"Command failed ({returncode}): {' '.join(cmd)}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def require_docker() -> None:
|
|
33
|
+
if shutil.which("docker") is None:
|
|
34
|
+
raise SystemExit("ERROR: docker not found in PATH")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(
|
|
38
|
+
cmd: list[str],
|
|
39
|
+
*,
|
|
40
|
+
cwd: Path | None = None,
|
|
41
|
+
env: dict[str, str] | None = None,
|
|
42
|
+
check: bool = True,
|
|
43
|
+
) -> int:
|
|
44
|
+
print(f"$ {' '.join(cmd)}", flush=True)
|
|
45
|
+
merged = os.environ.copy()
|
|
46
|
+
if env:
|
|
47
|
+
merged.update(env)
|
|
48
|
+
result = subprocess.run(cmd, cwd=cwd, env=merged)
|
|
49
|
+
if check and result.returncode != 0:
|
|
50
|
+
raise CommandError(cmd, result.returncode)
|
|
51
|
+
return result.returncode
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_capture(
|
|
55
|
+
cmd: list[str],
|
|
56
|
+
*,
|
|
57
|
+
cwd: Path | None = None,
|
|
58
|
+
env: dict[str, str] | None = None,
|
|
59
|
+
) -> subprocess.CompletedProcess[str]:
|
|
60
|
+
merged = os.environ.copy()
|
|
61
|
+
if env:
|
|
62
|
+
merged.update(env)
|
|
63
|
+
return subprocess.run(
|
|
64
|
+
cmd,
|
|
65
|
+
cwd=cwd,
|
|
66
|
+
env=merged,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
encoding=_SUBPROCESS_ENCODING,
|
|
70
|
+
errors="replace",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def image_exists(image: str) -> bool:
|
|
75
|
+
r = run_capture(["docker", "image", "inspect", image])
|
|
76
|
+
return r.returncode == 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_tee(
|
|
80
|
+
cmd: list[str],
|
|
81
|
+
log_file: Path,
|
|
82
|
+
*,
|
|
83
|
+
cwd: Path | None = None,
|
|
84
|
+
env: dict[str, str] | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Run command, stream stdout/stderr to console and log file."""
|
|
87
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
print(f"$ {' '.join(cmd)} -> {log_file}", flush=True)
|
|
89
|
+
merged = os.environ.copy()
|
|
90
|
+
if env:
|
|
91
|
+
merged.update(env)
|
|
92
|
+
with log_file.open("w", encoding="utf-8") as log:
|
|
93
|
+
proc = subprocess.Popen(
|
|
94
|
+
cmd,
|
|
95
|
+
cwd=cwd,
|
|
96
|
+
env=merged,
|
|
97
|
+
stdout=subprocess.PIPE,
|
|
98
|
+
stderr=subprocess.STDOUT,
|
|
99
|
+
text=True,
|
|
100
|
+
encoding=_SUBPROCESS_ENCODING,
|
|
101
|
+
errors="replace",
|
|
102
|
+
bufsize=1,
|
|
103
|
+
)
|
|
104
|
+
assert proc.stdout is not None
|
|
105
|
+
for line in proc.stdout:
|
|
106
|
+
_write_stdout(line)
|
|
107
|
+
log.write(line)
|
|
108
|
+
rc = proc.wait()
|
|
109
|
+
if rc != 0:
|
|
110
|
+
raise CommandError(cmd, rc)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def dir_size_human(path: Path) -> str:
|
|
114
|
+
"""Human-readable size. Skips broken symlinks (common on Windows yarn caches)."""
|
|
115
|
+
total = 0
|
|
116
|
+
try:
|
|
117
|
+
if path.is_file():
|
|
118
|
+
total = path.stat().st_size
|
|
119
|
+
elif path.is_dir():
|
|
120
|
+
for f in path.rglob("*"):
|
|
121
|
+
try:
|
|
122
|
+
# is_file() follows symlinks and can raise WinError 1920
|
|
123
|
+
if f.is_file():
|
|
124
|
+
total += f.stat().st_size
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
size = float(total)
|
|
130
|
+
for unit in ("B", "K", "M", "G"):
|
|
131
|
+
if size < 1024.0 or unit == "G":
|
|
132
|
+
if unit == "B":
|
|
133
|
+
return f"{int(size)}{unit}"
|
|
134
|
+
return f"{size:.1f}{unit}"
|
|
135
|
+
size /= 1024.0
|
|
136
|
+
return f"{size:.1f}G"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Download .deb packages into offline-cache/debs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from offline_docker import docker_cmd
|
|
8
|
+
from offline_docker.context import ProjectContext
|
|
9
|
+
from offline_docker.utils import docker_mount_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _build_debs_guest(ctx: ProjectContext) -> str:
|
|
13
|
+
pkgs_bash = " ".join(f'"{p}"' for p in ctx.config.deb_packages)
|
|
14
|
+
extra_repo_script = ""
|
|
15
|
+
for repo in ctx.config.deb_extra_repos:
|
|
16
|
+
gpg_name = Path(repo.gpg_dest).name
|
|
17
|
+
list_name = Path(repo.list_dest).name
|
|
18
|
+
extra_repo_script += f"""
|
|
19
|
+
echo ">>> Extra repo: {repo.name}"
|
|
20
|
+
curl -fsSL {repo.gpg_url} | gpg --dearmor -o /apt-meta/{gpg_name}
|
|
21
|
+
curl -fsSL {repo.list_url} > /apt-meta/{list_name}
|
|
22
|
+
cp /apt-meta/{gpg_name} /usr/share/keyrings/{gpg_name}
|
|
23
|
+
cp /apt-meta/{list_name} /etc/apt/sources.list.d/{list_name}
|
|
24
|
+
apt-get update
|
|
25
|
+
"""
|
|
26
|
+
# Optional apt mirrors from package resolver (ProGet deb / Nexus apt)
|
|
27
|
+
apt_mirror_script = ""
|
|
28
|
+
for i, mirror in enumerate(ctx.config.package_resolver().apt_mirrors()):
|
|
29
|
+
# Best-effort: prepend as extra signed-by-less list (corp mirrors vary)
|
|
30
|
+
apt_mirror_script += f"""
|
|
31
|
+
echo ">>> Apt mirror from sources[{i}]: {mirror}"
|
|
32
|
+
echo "deb [trusted=yes] {mirror} ./" > /etc/apt/sources.list.d/od-mirror-{i}.list
|
|
33
|
+
"""
|
|
34
|
+
return f"""
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
37
|
+
|
|
38
|
+
# Cache debs is bound to the guest distro: Ubuntu/Debian .deb несовместимы, и
|
|
39
|
+
# dpkg -i чужих пакетов ломает базовый образ. Тег образа может переехать
|
|
40
|
+
# (python:3.12-slim bookworm -> trixie) — сбрасываем кэш при смене базы.
|
|
41
|
+
. /etc/os-release
|
|
42
|
+
BASE_OS="$ID-$VERSION_ID"
|
|
43
|
+
if [ -f /debs/.base-os ] && [ "$(cat /debs/.base-os)" != "$BASE_OS" ]; then
|
|
44
|
+
echo ">>> База изменилась: $(cat /debs/.base-os) -> $BASE_OS - очищаю старые .deb"
|
|
45
|
+
find /debs -maxdepth 1 -name '*.deb' -delete
|
|
46
|
+
fi
|
|
47
|
+
printf '%s\\n' "$BASE_OS" > /debs/.base-os
|
|
48
|
+
|
|
49
|
+
apt-get update
|
|
50
|
+
apt-get install -y --no-install-recommends ca-certificates curl gnupg
|
|
51
|
+
{apt_mirror_script}
|
|
52
|
+
{extra_repo_script}
|
|
53
|
+
echo "ok-$(date +%s)" > /apt-meta/.ready
|
|
54
|
+
|
|
55
|
+
PKGS=({pkgs_bash})
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
echo ">>> Сбор транзитивных зависимостей"
|
|
59
|
+
ALL=""
|
|
60
|
+
for pkg in "${{PKGS[@]}}"; do
|
|
61
|
+
deps="$(apt-cache depends --recurse --no-recommends \\
|
|
62
|
+
--no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances \\
|
|
63
|
+
"$pkg" 2>/dev/null | grep -E "^[[:alnum:]][[:alnum:]._+-]*$" || true)"
|
|
64
|
+
ALL="$ALL $deps"
|
|
65
|
+
done
|
|
66
|
+
UNIQUE="$(echo "$ALL" | tr " " "\\n" | sort -u | grep -v "^$")"
|
|
67
|
+
|
|
68
|
+
cd /debs
|
|
69
|
+
total="$(echo "$UNIQUE" | wc -l)"
|
|
70
|
+
n=0
|
|
71
|
+
skipped=0
|
|
72
|
+
for pkg in $UNIQUE; do
|
|
73
|
+
n=$((n + 1))
|
|
74
|
+
if compgen -G "${{pkg}}_*.deb" > /dev/null \\
|
|
75
|
+
|| compgen -G "${{pkg}}-*.deb" > /dev/null; then
|
|
76
|
+
skipped=$((skipped + 1))
|
|
77
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
if (( n % 25 == 1 )); then
|
|
80
|
+
echo " [$n/$total] $pkg ..."
|
|
81
|
+
fi
|
|
82
|
+
apt-get download "$pkg" 2>/dev/null || echo " WARN: не скачан $pkg" >&2
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
count="$(find /debs -maxdepth 1 -name "*.deb" | wc -l)"
|
|
86
|
+
echo ""
|
|
87
|
+
echo "=== Готово: ${{count}} .deb в /debs (пропущено уже скачанных: ${{skipped}}) ==="
|
|
88
|
+
du -sh /debs
|
|
89
|
+
rm -rf /var/lib/apt/lists/*
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def download_debs(ctx: ProjectContext) -> None:
|
|
94
|
+
ctx.ensure_offline_dirs()
|
|
95
|
+
from offline_docker.sync_cache import maybe_materialize
|
|
96
|
+
|
|
97
|
+
maybe_materialize(ctx)
|
|
98
|
+
|
|
99
|
+
debs_docker = docker_mount_path(ctx.debs_dir)
|
|
100
|
+
apt_meta_docker = docker_mount_path(ctx.apt_meta_dir)
|
|
101
|
+
resolver = ctx.config.package_resolver()
|
|
102
|
+
|
|
103
|
+
print("=== download-debs ===")
|
|
104
|
+
print(f"Корень: {ctx.root}")
|
|
105
|
+
print(f"Offline cache: {ctx.offline_cache}")
|
|
106
|
+
print(f"Ubuntu image: {ctx.config.deb_ubuntu_image}")
|
|
107
|
+
print(f"Пакетов (seed): {len(ctx.config.deb_packages)}")
|
|
108
|
+
mirrors = resolver.apt_mirrors()
|
|
109
|
+
if mirrors:
|
|
110
|
+
print(f"Apt mirrors: {', '.join(mirrors)}")
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
docker_cmd.run(
|
|
114
|
+
[
|
|
115
|
+
"docker",
|
|
116
|
+
"run",
|
|
117
|
+
"--rm",
|
|
118
|
+
"-v",
|
|
119
|
+
f"{debs_docker}:/debs",
|
|
120
|
+
"-v",
|
|
121
|
+
f"{apt_meta_docker}:/apt-meta",
|
|
122
|
+
ctx.config.deb_ubuntu_image,
|
|
123
|
+
"bash",
|
|
124
|
+
"-ec",
|
|
125
|
+
_build_debs_guest(ctx),
|
|
126
|
+
],
|
|
127
|
+
env={"DOCKER_BUILDKIT": "0"},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if resolver.settings.resolve.fill_local:
|
|
131
|
+
from offline_docker.sync_cache import publish_to_remotes
|
|
132
|
+
|
|
133
|
+
publish_to_remotes(ctx)
|
|
134
|
+
|
|
135
|
+
if not (ctx.apt_meta_dir / ".ready").is_file():
|
|
136
|
+
raise SystemExit(
|
|
137
|
+
"ERROR: apt-meta не попал на хост - проверьте Docker mount path (D:/..., не /d/...)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
deb_count = len(list(ctx.debs_dir.glob("*.deb")))
|
|
141
|
+
print()
|
|
142
|
+
print(f"Всего .deb: {deb_count}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Pull and save Docker images into offline-cache/docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from offline_docker import docker_cmd
|
|
6
|
+
from offline_docker.context import ProjectContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def download_docker_images(ctx: ProjectContext) -> None:
|
|
10
|
+
ctx.ensure_offline_dirs()
|
|
11
|
+
print("=== download-docker-images ===")
|
|
12
|
+
print(f"Каталог: {ctx.docker_cache_dir}")
|
|
13
|
+
print()
|
|
14
|
+
|
|
15
|
+
for image in ctx.config.docker_images:
|
|
16
|
+
safe_name = image.replace("/", "_").replace(":", "-")
|
|
17
|
+
tar_path = ctx.docker_cache_dir / f"{safe_name}.tar"
|
|
18
|
+
print(f">>> pull {image}")
|
|
19
|
+
docker_cmd.run(["docker", "pull", image])
|
|
20
|
+
print(f">>> save -> {tar_path}")
|
|
21
|
+
docker_cmd.run(["docker", "save", "-o", str(tar_path), image])
|
|
22
|
+
print(docker_cmd.dir_size_human(tar_path))
|
|
23
|
+
print()
|
|
24
|
+
|
|
25
|
+
tar_count = len(list(ctx.docker_cache_dir.glob("*.tar")))
|
|
26
|
+
print(f"=== Готово: {tar_count} tar-файлов ===")
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Download npm cache for frontend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from offline_docker import docker_cmd
|
|
6
|
+
from offline_docker.context import ProjectContext
|
|
7
|
+
from offline_docker.utils import docker_mount_path, file_sha256
|
|
8
|
+
|
|
9
|
+
# POSIX /bin/sh (node:*-alpine has no bash; node ENTRYPOINT would otherwise exec "node bash").
|
|
10
|
+
# Не использовать pipefail: dash (Debian /bin/sh) его не поддерживает.
|
|
11
|
+
NPM_GUEST = r"""
|
|
12
|
+
set -eu
|
|
13
|
+
cp /app-src/package.json /app-src/package-lock.json . 2>/dev/null || cp /app-src/package.json .
|
|
14
|
+
if [ ! -f package-lock.json ]; then
|
|
15
|
+
echo "ERROR: package-lock.json не найден - нужен lockfile для offline npm ci" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
npm config set cache /npm-cache
|
|
19
|
+
npm config set registry "${NPM_REGISTRY}"
|
|
20
|
+
# HTTP corporate registries (e.g. Nexus without TLS)
|
|
21
|
+
case "${NPM_REGISTRY}" in
|
|
22
|
+
http://*) npm config set strict-ssl false ;;
|
|
23
|
+
esac
|
|
24
|
+
npm ci --no-audit --no-fund
|
|
25
|
+
echo "mount-check-$(date +%s)" > /npm-cache/.mount-check
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def download_npm_cache(ctx: ProjectContext) -> None:
|
|
30
|
+
fe = ctx.config.frontend_path()
|
|
31
|
+
lock = ctx.config.frontend_lock_path()
|
|
32
|
+
if fe is None or lock is None:
|
|
33
|
+
print("=== download-npm-cache: SKIP (frontend/package-lock.json не найден) ===")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
ctx.ensure_offline_dirs()
|
|
37
|
+
hash_file = ctx.npm_cache_dir / ".package-lock.sha256"
|
|
38
|
+
current = file_sha256(lock)
|
|
39
|
+
cacache = ctx.npm_cache_dir / "_cacache"
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
hash_file.is_file()
|
|
43
|
+
and hash_file.read_text(encoding="utf-8").strip() == current
|
|
44
|
+
and cacache.is_dir()
|
|
45
|
+
):
|
|
46
|
+
print("=== download-npm-cache: package-lock.json не менялся, cache актуален ===")
|
|
47
|
+
print(docker_cmd.dir_size_human(ctx.npm_cache_dir))
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
from offline_docker.sync_cache import maybe_materialize
|
|
51
|
+
|
|
52
|
+
maybe_materialize(ctx)
|
|
53
|
+
|
|
54
|
+
resolver = ctx.config.package_resolver()
|
|
55
|
+
registry = resolver.primary_npm_registry() or "https://registry.npmjs.org/"
|
|
56
|
+
used_default = resolver.primary_npm_registry() is None
|
|
57
|
+
|
|
58
|
+
frontend_docker = docker_mount_path(fe)
|
|
59
|
+
npm_docker = docker_mount_path(ctx.npm_cache_dir)
|
|
60
|
+
|
|
61
|
+
print("=== download-npm-cache ===")
|
|
62
|
+
print(f"Frontend: {fe}")
|
|
63
|
+
print(f"npm cache dir: {ctx.npm_cache_dir}")
|
|
64
|
+
print(f"Node image: {ctx.config.node_image}")
|
|
65
|
+
print(f"Resolve: {' -> '.join(resolver.settings.resolve.order)}")
|
|
66
|
+
print(f"npm registry: {registry}")
|
|
67
|
+
if used_default and resolver.is_local_only():
|
|
68
|
+
print(
|
|
69
|
+
"WARN: sources.json = local-only; для download используется "
|
|
70
|
+
"https://registry.npmjs.org/"
|
|
71
|
+
)
|
|
72
|
+
print(
|
|
73
|
+
" Явный режим: offdock configure --with-upstream"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
docker_cmd.run(
|
|
77
|
+
[
|
|
78
|
+
"docker",
|
|
79
|
+
"run",
|
|
80
|
+
"--rm",
|
|
81
|
+
"--entrypoint",
|
|
82
|
+
"/bin/sh",
|
|
83
|
+
"-e",
|
|
84
|
+
f"NPM_REGISTRY={registry}",
|
|
85
|
+
"-v",
|
|
86
|
+
f"{frontend_docker}:/app-src:ro",
|
|
87
|
+
"-v",
|
|
88
|
+
f"{npm_docker}:/npm-cache",
|
|
89
|
+
"-w",
|
|
90
|
+
"/work",
|
|
91
|
+
ctx.config.node_image,
|
|
92
|
+
"-ec",
|
|
93
|
+
NPM_GUEST,
|
|
94
|
+
],
|
|
95
|
+
env={"DOCKER_BUILDKIT": "0", "MSYS_NO_PATHCONV": "1"},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
hash_file.write_text(current + "\n", encoding="utf-8")
|
|
99
|
+
print()
|
|
100
|
+
print(f"=== Готово: npm cache в {ctx.npm_cache_dir} ===")
|
|
101
|
+
print(docker_cmd.dir_size_human(ctx.npm_cache_dir))
|
|
102
|
+
|
|
103
|
+
if resolver.settings.resolve.fill_local:
|
|
104
|
+
from offline_docker.sync_cache import publish_to_remotes
|
|
105
|
+
|
|
106
|
+
publish_to_remotes(ctx)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Download Trivy vulnerability DB into offline-cache/trivy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from offline_docker import docker_cmd
|
|
8
|
+
from offline_docker.context import ProjectContext
|
|
9
|
+
from offline_docker.utils import docker_mount_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def trivy_db_stale(ctx: ProjectContext) -> bool:
|
|
13
|
+
"""True if DB is missing or older than configured max age."""
|
|
14
|
+
cache = ctx.trivy_cache_dir
|
|
15
|
+
ts_file = cache / ".trivy-db.timestamp"
|
|
16
|
+
max_age = ctx.config.trivy.db_max_age_days * 86400
|
|
17
|
+
|
|
18
|
+
if not cache.is_dir():
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
others = [
|
|
22
|
+
p
|
|
23
|
+
for p in cache.rglob("*")
|
|
24
|
+
if p.is_file() and p.name != ".trivy-db.timestamp"
|
|
25
|
+
]
|
|
26
|
+
if not others:
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
if not ts_file.is_file():
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
stamp = int(ts_file.read_text(encoding="utf-8").strip())
|
|
34
|
+
except ValueError:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
return (time.time() - stamp) > max_age
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ensure_trivy_image(ctx: ProjectContext) -> None:
|
|
41
|
+
image = ctx.config.trivy.scanner_image
|
|
42
|
+
if docker_cmd.image_exists(image):
|
|
43
|
+
return
|
|
44
|
+
from offline_docker.load_images import load_docker_images
|
|
45
|
+
|
|
46
|
+
print(f"Образ {image} не найден — load-docker-images")
|
|
47
|
+
load_docker_images(ctx)
|
|
48
|
+
if not docker_cmd.image_exists(image):
|
|
49
|
+
raise SystemExit(
|
|
50
|
+
f"ERROR: образ {image} недоступен. Выполните offdock cache images"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def download_trivy_db(ctx: ProjectContext) -> None:
|
|
55
|
+
ctx.ensure_offline_dirs()
|
|
56
|
+
if not trivy_db_stale(ctx):
|
|
57
|
+
print(
|
|
58
|
+
f"=== download-trivy-db: БД актуальна (моложе {ctx.config.trivy.db_max_age_days} дн.) ==="
|
|
59
|
+
)
|
|
60
|
+
print(docker_cmd.dir_size_human(ctx.trivy_cache_dir))
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
print("=== download-trivy-db ===")
|
|
64
|
+
print(f"Каталог: {ctx.trivy_cache_dir}")
|
|
65
|
+
print(f"Образ: {ctx.config.trivy.scanner_image}")
|
|
66
|
+
print()
|
|
67
|
+
|
|
68
|
+
ensure_trivy_image(ctx)
|
|
69
|
+
cache_docker = docker_mount_path(ctx.trivy_cache_dir)
|
|
70
|
+
|
|
71
|
+
docker_cmd.run(
|
|
72
|
+
[
|
|
73
|
+
"docker",
|
|
74
|
+
"run",
|
|
75
|
+
"--rm",
|
|
76
|
+
"-v",
|
|
77
|
+
f"{cache_docker}:/root/.cache/trivy",
|
|
78
|
+
ctx.config.trivy.scanner_image,
|
|
79
|
+
"image",
|
|
80
|
+
"--download-db-only",
|
|
81
|
+
],
|
|
82
|
+
env={"MSYS_NO_PATHCONV": "1"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
(ctx.trivy_cache_dir / ".trivy-db.timestamp").write_text(
|
|
86
|
+
str(int(time.time())) + "\n", encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
print()
|
|
89
|
+
print(f"=== Готово: vulnerability DB в {ctx.trivy_cache_dir} ===")
|
|
90
|
+
print(docker_cmd.dir_size_human(ctx.trivy_cache_dir))
|