Merge branch 'master' of gitlab.sarex.io:infra/iac
This commit is contained in:
commit
3dadb7a87a
@ -4,6 +4,8 @@ include:
|
||||
|
||||
stages:
|
||||
- build
|
||||
- slices
|
||||
- slices-publish
|
||||
|
||||
variables:
|
||||
RELEASE_NAME: iac-offline
|
||||
@ -33,3 +35,31 @@ build_offline_mirror:
|
||||
- docker push ${IMAGE_NAME}
|
||||
- docker push ${CR}/${RELEASE_NAME}:latest
|
||||
timeout: 30m
|
||||
|
||||
generate_slice_jobs:
|
||||
stage: slices
|
||||
image: ${CR}/infra/slice-builder:1.0.0
|
||||
tags:
|
||||
- build
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG =~ /^v?(?:\d+\.){2}\d+$/
|
||||
variables:
|
||||
GIT_DEPTH: "1"
|
||||
script:
|
||||
- python3 ci/publish-slice.py generate --release "$CI_COMMIT_TAG" --commit "$CI_COMMIT_SHA" --out slices-pipeline.yml
|
||||
- cat slices-pipeline.yml
|
||||
artifacts:
|
||||
paths:
|
||||
- slices-pipeline.yml
|
||||
|
||||
publish_contour_slices:
|
||||
stage: slices-publish
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG =~ /^v?(?:\d+\.){2}\d+$/
|
||||
needs:
|
||||
- generate_slice_jobs
|
||||
trigger:
|
||||
include:
|
||||
- artifact: slices-pipeline.yml
|
||||
job: generate_slice_jobs
|
||||
strategy: depend
|
||||
|
||||
@ -36,7 +36,7 @@ spec:
|
||||
|
||||
image:
|
||||
name:
|
||||
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations-api-files:prod_3f19a21d
|
||||
_default: cr.yandex/crp3ccidau046kdj8g9q/documentations-api-files:prod_fef3add2
|
||||
pullPolicy:
|
||||
_default: IfNotPresent
|
||||
|
||||
|
||||
163
ci/build-slice.py
Normal file
163
ci/build-slice.py
Normal file
@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import io
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
import yaml
|
||||
|
||||
FLUX_KUSTOMIZATION = "kustomize.toolkit.fluxcd.io"
|
||||
KUSTOMIZATION_FILES = ("kustomization.yaml", "kustomization.yml", "Kustomization")
|
||||
|
||||
|
||||
class SliceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def is_remote(ref: str) -> bool:
|
||||
return "://" in ref or ref.startswith(("git@", "github.com/", "gitlab.com/"))
|
||||
|
||||
|
||||
def kustomization_file(directory: Path) -> Path | None:
|
||||
for name in KUSTOMIZATION_FILES:
|
||||
candidate = directory / name
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def local_refs(doc: dict) -> list[str]:
|
||||
refs = []
|
||||
refs += doc.get("resources") or []
|
||||
refs += doc.get("components") or []
|
||||
refs += doc.get("patchesStrategicMerge") or []
|
||||
refs += [p["path"] for p in doc.get("patches") or [] if isinstance(p, dict) and "path" in p]
|
||||
for key in ("configMapGenerator", "secretGenerator"):
|
||||
for gen in doc.get(key) or []:
|
||||
for entry in (gen.get("files") or []) + (gen.get("envs") or []):
|
||||
refs.append(entry.split("=", 1)[-1])
|
||||
if gen.get("env"):
|
||||
refs.append(gen["env"])
|
||||
return [r for r in refs if isinstance(r, str)]
|
||||
|
||||
|
||||
def flux_paths(path: Path) -> list[str]:
|
||||
try:
|
||||
docs = list(yaml.safe_load_all(path.read_text()))
|
||||
except yaml.YAMLError:
|
||||
return []
|
||||
return [
|
||||
d["spec"]["path"]
|
||||
for d in docs
|
||||
if isinstance(d, dict)
|
||||
and str(d.get("apiVersion", "")).startswith(FLUX_KUSTOMIZATION)
|
||||
and d.get("kind") == "Kustomization"
|
||||
and d.get("spec", {}).get("path")
|
||||
]
|
||||
|
||||
|
||||
def collect(repo: Path, start: str) -> tuple[set[PurePosixPath], list[str]]:
|
||||
repo = repo.resolve()
|
||||
files: set[PurePosixPath] = set()
|
||||
entries: list[str] = []
|
||||
seen_dirs: set[Path] = set()
|
||||
|
||||
def rel(p: Path) -> PurePosixPath:
|
||||
try:
|
||||
return PurePosixPath(p.resolve().relative_to(repo).as_posix())
|
||||
except ValueError:
|
||||
raise SliceError(f"reference outside the repository: {p}")
|
||||
|
||||
def add_file(p: Path):
|
||||
r = rel(p)
|
||||
if r in files:
|
||||
return
|
||||
files.add(r)
|
||||
if p.suffix in (".yaml", ".yml"):
|
||||
for fp in flux_paths(p):
|
||||
visit_dir(repo / fp, entry=True)
|
||||
|
||||
def visit_dir(d: Path, entry: bool = False):
|
||||
d = d.resolve()
|
||||
if d in seen_dirs:
|
||||
return
|
||||
seen_dirs.add(d)
|
||||
kfile = kustomization_file(d)
|
||||
if kfile is None:
|
||||
raise SliceError(f"no kustomization.yaml in {rel(d)}")
|
||||
if entry:
|
||||
entries.append(str(rel(d)))
|
||||
add_file(kfile)
|
||||
doc = yaml.safe_load(kfile.read_text()) or {}
|
||||
for ref in local_refs(doc):
|
||||
if is_remote(ref):
|
||||
raise SliceError(f"remote reference {ref} in {rel(kfile)}")
|
||||
target = (d / ref)
|
||||
if target.is_dir():
|
||||
visit_dir(target)
|
||||
elif target.is_file():
|
||||
add_file(target)
|
||||
else:
|
||||
raise SliceError(f"reference {ref} in {rel(kfile)} does not exist")
|
||||
|
||||
visit_dir(repo / start, entry=True)
|
||||
return files, sorted(set(entries))
|
||||
|
||||
|
||||
def write_tar(repo: Path, files: set[PurePosixPath], out: Path):
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w", format=tarfile.PAX_FORMAT) as tar:
|
||||
for f in sorted(files):
|
||||
data = (repo / f).read_bytes()
|
||||
info = tarfile.TarInfo(str(f))
|
||||
info.size = len(data)
|
||||
info.mode = 0o644
|
||||
info.mtime = 0
|
||||
info.uid = info.gid = 0
|
||||
info.uname = info.gname = ""
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
out.write_bytes(buf.getvalue())
|
||||
|
||||
|
||||
def kustomize_build(path: Path) -> bytes:
|
||||
p = subprocess.run(["kubectl", "kustomize", str(path)], capture_output=True)
|
||||
if p.returncode != 0:
|
||||
raise SliceError(f"kustomize build {path} failed: {p.stderr.decode().strip()}")
|
||||
return p.stdout
|
||||
|
||||
|
||||
def verify(repo: Path, out: Path, entries: list[str]):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with tarfile.open(out) as tar:
|
||||
tar.extractall(tmp, filter="data")
|
||||
for e in entries:
|
||||
if kustomize_build(repo / e) != kustomize_build(Path(tmp) / e):
|
||||
raise SliceError(f"kustomize build of {e} differs between slice and full repository")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("contour")
|
||||
ap.add_argument("--repo", default=".")
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--no-verify", action="store_true")
|
||||
args = ap.parse_args()
|
||||
repo = Path(args.repo).resolve()
|
||||
out = Path(args.out)
|
||||
try:
|
||||
files, entries = collect(repo, f"clusters/{args.contour}")
|
||||
write_tar(repo, files, out)
|
||||
if not args.no_verify:
|
||||
verify(repo, out, entries)
|
||||
except SliceError as e:
|
||||
print(f"slice {args.contour}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"slice {args.contour}: {len(files)} files, entry points: {', '.join(entries)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
217
ci/publish-slice.py
Normal file
217
ci/publish-slice.py
Normal file
@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
spec = importlib.util.spec_from_file_location("build_slice", Path(__file__).with_name("build-slice.py"))
|
||||
bs = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(bs)
|
||||
|
||||
REGISTRY_HOST = "cr.yandex"
|
||||
SLICE_REGISTRY = "crpct2b2lsmao4nm0cjk"
|
||||
CONFIG_TYPE = "application/vnd.sarex.slice.config.v1+json"
|
||||
LAYER_TYPE = "application/vnd.oci.image.layer.v1.tar"
|
||||
MANIFEST_TYPE = "application/vnd.oci.image.manifest.v1+json"
|
||||
JOB_IMAGE = "${CR}/infra/slice-builder:1.0.0"
|
||||
|
||||
|
||||
class PublishError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def digest(data: bytes) -> str:
|
||||
return "sha256:" + hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def list_contours(repo: Path) -> list[str]:
|
||||
return sorted(p.parent.name for p in repo.glob("clusters/*/kustomization.yaml"))
|
||||
|
||||
|
||||
def child_pipeline(contours: list[str], release: str, commit: str) -> str:
|
||||
jobs: dict = {"stages": ["slices"]}
|
||||
for c in contours:
|
||||
jobs[f"slice {c}"] = {
|
||||
"stage": "slices",
|
||||
"image": JOB_IMAGE,
|
||||
"tags": ["build"],
|
||||
"allow_failure": True,
|
||||
"variables": {"GIT_DEPTH": "1"},
|
||||
"script": [
|
||||
"printf '%s' \"$SLICES_PUSHER_KEY\" | base64 -d > /tmp/slices-key.json",
|
||||
f"python3 ci/publish-slice.py publish --contour {c} --release {release} --commit {commit} --key-file /tmp/slices-key.json",
|
||||
],
|
||||
"after_script": ["rm -f /tmp/slices-key.json"],
|
||||
}
|
||||
if not contours:
|
||||
jobs["no contours"] = {"stage": "slices", "image": JOB_IMAGE, "tags": ["build"], "script": ["echo 'no clusters/*/kustomization.yaml found'"]}
|
||||
return yaml.safe_dump(jobs, sort_keys=False, allow_unicode=False)
|
||||
|
||||
|
||||
def build_manifest(layer: bytes, annotations: dict[str, str]) -> tuple[bytes, bytes]:
|
||||
config = b"{}"
|
||||
manifest = {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": MANIFEST_TYPE,
|
||||
"config": {"mediaType": CONFIG_TYPE, "digest": digest(config), "size": len(config)},
|
||||
"layers": [{
|
||||
"mediaType": LAYER_TYPE,
|
||||
"digest": digest(layer),
|
||||
"size": len(layer),
|
||||
"annotations": {"org.opencontainers.image.title": "slice.tar"},
|
||||
}],
|
||||
"annotations": dict(sorted(annotations.items())),
|
||||
}
|
||||
return config, json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
class Registry:
|
||||
def __init__(self, username: str, password: str):
|
||||
self.base = f"https://{REGISTRY_HOST}"
|
||||
self.basic = "Basic " + base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||
self.tokens: dict[str, str] = {}
|
||||
|
||||
def _send(self, method, url, data=None, headers=None):
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers or {})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return r.status, dict(r.headers), r.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, dict(e.headers), e.read()
|
||||
|
||||
def _token(self, challenge: str, scope: str) -> str:
|
||||
params = dict(re.findall(r'(\w+)="([^"]*)"', challenge))
|
||||
realm = params.pop("realm")
|
||||
params["scope"] = scope
|
||||
status, _, body = self._send("GET", f"{realm}?{urllib.parse.urlencode(params)}", headers={"Authorization": self.basic})
|
||||
if status != 200:
|
||||
raise PublishError(f"registry auth failed: HTTP {status}")
|
||||
payload = json.loads(body)
|
||||
return payload.get("token") or payload.get("access_token")
|
||||
|
||||
def request(self, method, repo, path, data=None, headers=None):
|
||||
scope = f"repository:{repo}:pull,push"
|
||||
url = path if path.startswith("http") else f"{self.base}/v2/{repo}/{path}"
|
||||
headers = dict(headers or {})
|
||||
if scope in self.tokens:
|
||||
headers["Authorization"] = f"Bearer {self.tokens[scope]}"
|
||||
status, h, body = self._send(method, url, data, headers)
|
||||
if status == 401:
|
||||
challenge = {k.lower(): v for k, v in h.items()}.get("www-authenticate", "")
|
||||
self.tokens[scope] = self._token(challenge, scope)
|
||||
headers["Authorization"] = f"Bearer {self.tokens[scope]}"
|
||||
status, h, body = self._send(method, url, data, headers)
|
||||
return status, {k.lower(): v for k, v in h.items()}, body
|
||||
|
||||
def current_layer(self, repo: str) -> str | None:
|
||||
status, _, body = self.request("GET", repo, "manifests/current", headers={"Accept": MANIFEST_TYPE})
|
||||
if status == 404:
|
||||
return None
|
||||
if status != 200:
|
||||
raise PublishError(f"{repo}:current: HTTP {status}")
|
||||
return json.loads(body)["layers"][0]["digest"]
|
||||
|
||||
def put_blob(self, repo: str, data: bytes) -> bool:
|
||||
d = digest(data)
|
||||
if self.request("HEAD", repo, f"blobs/{d}")[0] == 200:
|
||||
return False
|
||||
status, h, _ = self.request("POST", repo, "blobs/uploads/")
|
||||
if status != 202:
|
||||
raise PublishError(f"blob upload to {repo}: HTTP {status}")
|
||||
location = h["location"]
|
||||
if location.startswith("/"):
|
||||
location = self.base + location
|
||||
sep = "&" if "?" in location else "?"
|
||||
status, _, _ = self.request("PUT", repo, f"{location}{sep}digest={d}", data, {"Content-Type": "application/octet-stream"})
|
||||
if status != 201:
|
||||
raise PublishError(f"blob upload to {repo}: HTTP {status}")
|
||||
return True
|
||||
|
||||
def push(self, repo: str, tags: list[str], layer: bytes, annotations: dict[str, str]) -> bool:
|
||||
config, manifest = build_manifest(layer, annotations)
|
||||
self.put_blob(repo, config)
|
||||
uploaded = self.put_blob(repo, layer)
|
||||
for tag in tags:
|
||||
status, _, body = self.request("PUT", repo, f"manifests/{tag}", manifest, {"Content-Type": MANIFEST_TYPE})
|
||||
if status != 201:
|
||||
raise PublishError(f"{repo}:{tag}: HTTP {status} {body[:200]!r}")
|
||||
return uploaded
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||
|
||||
|
||||
def publish(repo: Path, contour: str, release: str, commit: str, key_file: Path) -> int:
|
||||
target = f"{SLICE_REGISTRY}/{contour}-iac"
|
||||
log(f"contour {contour}, source iac@{commit[:8]}, release {release}")
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp) / "slice.tar"
|
||||
files, entries = bs.collect(repo, f"clusters/{contour}")
|
||||
log(f"collected {len(files)} files, entry points: {', '.join(entries)}")
|
||||
bs.write_tar(repo, files, out)
|
||||
bs.verify(repo, out, entries)
|
||||
log(f"verified: kustomize build of {len(entries)} entry point(s) identical to full repository")
|
||||
layer = out.read_bytes()
|
||||
new = digest(layer)
|
||||
log(f"slice size {len(layer)} bytes, digest {new}")
|
||||
registry = Registry("json_key", key_file.read_text())
|
||||
current = registry.current_layer(f"{target}")
|
||||
changed = current != new
|
||||
log(f"registry {REGISTRY_HOST}/{target}, current " + (current or "absent") + (", content changed" if changed else ", content unchanged"))
|
||||
tags = [commit[:8], release] + (["current"] if changed else [])
|
||||
uploaded = registry.push(target, tags, layer, {
|
||||
"org.opencontainers.image.source": "iac",
|
||||
"org.opencontainers.image.revision": commit,
|
||||
"org.opencontainers.image.version": release,
|
||||
"org.opencontainers.image.created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"io.sarex.slice.contour": contour,
|
||||
"io.sarex.slice.kind": "iac",
|
||||
})
|
||||
log(f"layer {'uploaded' if uploaded else 'already in registry'}, tags pushed: {', '.join(tags)}")
|
||||
except (bs.SliceError, PublishError) as e:
|
||||
log(f"FAILED: {e}")
|
||||
return 1
|
||||
log("done")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
gen = sub.add_parser("generate")
|
||||
gen.add_argument("--repo", default=".")
|
||||
gen.add_argument("--release", required=True)
|
||||
gen.add_argument("--commit", required=True)
|
||||
gen.add_argument("--out", required=True)
|
||||
pub = sub.add_parser("publish")
|
||||
pub.add_argument("--repo", default=".")
|
||||
pub.add_argument("--contour", required=True)
|
||||
pub.add_argument("--release", required=True)
|
||||
pub.add_argument("--commit", required=True)
|
||||
pub.add_argument("--key-file", required=True)
|
||||
args = ap.parse_args()
|
||||
repo = Path(args.repo).resolve()
|
||||
if args.cmd == "generate":
|
||||
contours = list_contours(repo)
|
||||
Path(args.out).write_text(child_pipeline(contours, args.release, args.commit))
|
||||
log(f"{len(contours)} contours: {', '.join(contours)}")
|
||||
return 0
|
||||
return publish(repo, args.contour, args.release, args.commit, Path(args.key_file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
93
ci/tests/test_build_slice.py
Normal file
93
ci/tests/test_build_slice.py
Normal file
@ -0,0 +1,93 @@
|
||||
import importlib.util
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
spec = importlib.util.spec_from_file_location("build_slice", Path(__file__).parents[1] / "build-slice.py")
|
||||
bs = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(bs)
|
||||
|
||||
|
||||
def put(root: Path, rel: str, text: str):
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(text)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path):
|
||||
put(tmp_path, "clusters/a/kustomization.yaml", "resources:\n - ./flux-system\n - ./layers.yaml\n")
|
||||
put(tmp_path, "clusters/a/flux-system/kustomization.yaml", "resources:\n - gotk-sync.yaml\n")
|
||||
put(tmp_path, "clusters/a/flux-system/gotk-sync.yaml",
|
||||
"apiVersion: kustomize.toolkit.fluxcd.io/v1\nkind: Kustomization\nspec:\n path: ./clusters/a\n")
|
||||
put(tmp_path, "clusters/a/layers.yaml",
|
||||
"apiVersion: kustomize.toolkit.fluxcd.io/v1\nkind: Kustomization\nspec:\n path: ./layers/a/00-vault\n")
|
||||
put(tmp_path, "layers/a/00-vault/kustomization.yaml", "resources:\n - ../../../infrastructure/vault/contour-template\n")
|
||||
put(tmp_path, "infrastructure/vault/contour-template/kustomization.yaml",
|
||||
"resources:\n - ../base\npatches:\n - path: vault.yaml\n")
|
||||
put(tmp_path, "infrastructure/vault/contour-template/vault.yaml", "kind: HelmRelease\n")
|
||||
put(tmp_path, "infrastructure/vault/base/kustomization.yaml", "resources:\n - helmrelease.yaml\n")
|
||||
put(tmp_path, "infrastructure/vault/base/helmrelease.yaml", "kind: HelmRelease\n")
|
||||
put(tmp_path, "infrastructure/vault/base/unused.yaml", "kind: ConfigMap\n")
|
||||
put(tmp_path, "infrastructure/vault/uralkal/kustomization.yaml", "resources:\n - ../base\n")
|
||||
put(tmp_path, "clusters/b/kustomization.yaml", "resources:\n - ../../infrastructure/vault/uralkal\n")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_slice_follows_flux_paths_and_skips_foreign(repo):
|
||||
files, entries = bs.collect(repo, "clusters/a")
|
||||
names = {str(f) for f in files}
|
||||
assert "infrastructure/vault/base/helmrelease.yaml" in names
|
||||
assert "infrastructure/vault/contour-template/vault.yaml" in names
|
||||
assert "layers/a/00-vault/kustomization.yaml" in names
|
||||
assert not any("uralkal" in n or n.startswith("clusters/b") for n in names)
|
||||
assert "infrastructure/vault/base/unused.yaml" not in names
|
||||
assert entries == ["clusters/a", "layers/a/00-vault"]
|
||||
|
||||
|
||||
def test_remote_reference_stops_build(repo):
|
||||
put(repo, "clusters/a/kustomization.yaml", "resources:\n - https://github.com/x/y//z\n")
|
||||
with pytest.raises(bs.SliceError, match="remote reference"):
|
||||
bs.collect(repo, "clusters/a")
|
||||
|
||||
|
||||
def test_dangling_reference_stops_build(repo):
|
||||
put(repo, "clusters/a/kustomization.yaml", "resources:\n - ./missing.yaml\n")
|
||||
with pytest.raises(bs.SliceError, match="does not exist"):
|
||||
bs.collect(repo, "clusters/a")
|
||||
|
||||
|
||||
def test_tar_is_byte_identical_across_runs(repo, tmp_path_factory):
|
||||
files, _ = bs.collect(repo, "clusters/a")
|
||||
out1 = tmp_path_factory.mktemp("o") / "s.tar"
|
||||
out2 = tmp_path_factory.mktemp("o") / "s.tar"
|
||||
bs.write_tar(repo, files, out1)
|
||||
(repo / "clusters/a/layers.yaml").touch()
|
||||
bs.write_tar(repo, files, out2)
|
||||
assert out1.read_bytes() == out2.read_bytes()
|
||||
with tarfile.open(out1) as t:
|
||||
assert all(m.mtime == 0 for m in t.getmembers())
|
||||
|
||||
|
||||
|
||||
pspec = importlib.util.spec_from_file_location("publish_slice", Path(__file__).parents[1] / "publish-slice.py")
|
||||
ps = importlib.util.module_from_spec(pspec)
|
||||
pspec.loader.exec_module(ps)
|
||||
|
||||
|
||||
def test_every_cluster_with_kustomization_is_a_contour(tmp_path):
|
||||
put(tmp_path, "clusters/stand/kustomization.yaml", "resources: []\n")
|
||||
put(tmp_path, "clusters/vad/kustomization.yaml", "resources: []\n")
|
||||
put(tmp_path, "clusters/contour/apps.yaml", "kind: Kustomization\n")
|
||||
assert ps.list_contours(tmp_path) == ["stand", "vad"]
|
||||
|
||||
|
||||
def test_child_pipeline_has_one_job_per_contour():
|
||||
doc = yaml.safe_load(ps.child_pipeline(["stand", "vad"], "3.9.68", "abcdef1234"))
|
||||
jobs = {k: v for k, v in doc.items() if k != "stages"}
|
||||
assert sorted(jobs) == ["slice stand", "slice vad"]
|
||||
assert all(j["allow_failure"] for j in jobs.values())
|
||||
assert "--contour vad --release 3.9.68 --commit abcdef1234" in jobs["slice vad"]["script"][1]
|
||||
assert all(ord(ch) < 128 for ch in ps.child_pipeline(["stand"], "1.0.0", "a" * 40))
|
||||
4
clusters/stand/00-vault/kustomization.yaml
Normal file
4
clusters/stand/00-vault/kustomization.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../../infrastructure/vault/contour-template
|
||||
6440
clusters/stand/flux-system/gotk-components.yaml
Normal file
6440
clusters/stand/flux-system/gotk-components.yaml
Normal file
File diff suppressed because it is too large
Load Diff
25
clusters/stand/flux-system/gotk-sync.yaml
Normal file
25
clusters/stand/flux-system/gotk-sync.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m0s
|
||||
ref:
|
||||
branch: main
|
||||
url: http://gitea.gitea.svc:3000/infra/iac.git
|
||||
secretRef:
|
||||
name: flux-system
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m0s
|
||||
path: ./clusters/stand
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
5
clusters/stand/flux-system/kustomization.yaml
Normal file
5
clusters/stand/flux-system/kustomization.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- gotk-components.yaml
|
||||
- gotk-sync.yaml
|
||||
12
clusters/stand/helm-repositories.yaml
Normal file
12
clusters/stand/helm-repositories.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: yc-oci-charts
|
||||
namespace: flux-system
|
||||
spec:
|
||||
type: oci
|
||||
interval: 10m0s
|
||||
url: oci://cr.yandex/crp3ccidau046kdj8g9q/charts
|
||||
secretRef:
|
||||
name: yc-cr-auth
|
||||
6
clusters/stand/kustomization.yaml
Normal file
6
clusters/stand/kustomization.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ./flux-system
|
||||
- ./helm-repositories.yaml
|
||||
- ./layers.yaml
|
||||
17
clusters/stand/layers.yaml
Normal file
17
clusters/stand/layers.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: 00-vault
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m0s
|
||||
timeout: 10m0s
|
||||
path: ./clusters/stand/00-vault
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
postBuild:
|
||||
substituteFrom:
|
||||
- kind: ConfigMap
|
||||
name: contour-vars
|
||||
6
infrastructure/vault/contour-template/kustomization.yaml
Normal file
6
infrastructure/vault/contour-template/kustomization.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../base
|
||||
patches:
|
||||
- path: vault.yaml
|
||||
110
infrastructure/vault/contour-template/vault.yaml
Normal file
110
infrastructure/vault/contour-template/vault.yaml
Normal file
@ -0,0 +1,110 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: vault
|
||||
namespace: vault
|
||||
spec:
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
disableWait: true
|
||||
upgrade:
|
||||
disableWait: true
|
||||
postRenderers:
|
||||
- kustomize:
|
||||
patches:
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: StatefulSet
|
||||
namespace: vault
|
||||
name: vault-vault-contour
|
||||
patch: |-
|
||||
- op: replace
|
||||
path: /spec/template/spec/nodeSelector
|
||||
value:
|
||||
dedicated: generic
|
||||
- op: replace
|
||||
path: /spec/template/spec/tolerations
|
||||
value: []
|
||||
values:
|
||||
global:
|
||||
namespace: vault
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
backup:
|
||||
enabled: false
|
||||
injector:
|
||||
nodeSelector:
|
||||
dedicated: generic
|
||||
tolerations: []
|
||||
server:
|
||||
includeConfigAnnotation: true
|
||||
nodeSelector:
|
||||
dedicated: generic
|
||||
tolerations: []
|
||||
dataStorage:
|
||||
size: 10Gi
|
||||
storageClass: ${STORAGE_CLASS}
|
||||
volumes:
|
||||
- name: unseal-keys
|
||||
secret:
|
||||
secretName: vault-unseal-keys
|
||||
optional: true
|
||||
defaultMode: 0440
|
||||
extraContainers:
|
||||
- name: unsealer
|
||||
image: cr.yandex/crp3ccidau046kdj8g9q/vault/vault:1.21.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: VAULT_ADDR
|
||||
value: http://127.0.0.1:8200
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
while true; do
|
||||
if vault status >/dev/null 2>&1; then
|
||||
:
|
||||
else
|
||||
rc=$?
|
||||
if [ "$rc" -eq 2 ]; then
|
||||
if [ -s /vault/unseal-keys/key1 ] && [ -s /vault/unseal-keys/key2 ] && [ -s /vault/unseal-keys/key3 ]; then
|
||||
for k in key1 key2 key3; do
|
||||
vault operator unseal "$(cat /vault/unseal-keys/$k)" >/dev/null 2>&1 || echo "unseal call failed"
|
||||
done
|
||||
if vault status >/dev/null 2>&1; then echo "unsealed"; else echo "still sealed"; fi
|
||||
else
|
||||
echo "sealed, keys not mounted"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
volumeMounts:
|
||||
- name: unseal-keys
|
||||
mountPath: /vault/unseal-keys
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
ha:
|
||||
replicas: 1
|
||||
raft:
|
||||
config: |
|
||||
ui = true
|
||||
|
||||
listener "tcp" {
|
||||
tls_disable = 1
|
||||
address = "[::]:8200"
|
||||
cluster_address = "[::]:8201"
|
||||
}
|
||||
|
||||
storage "raft" {
|
||||
path = "/vault/data"
|
||||
}
|
||||
|
||||
service_registration "kubernetes" {}
|
||||
Loading…
Reference in New Issue
Block a user