Initial commit
Label Sync / Label Sync (push) Failing after 1m7s
E2E / reject-invalid (bad-bgp-asn) (push) Skipped
E2E / reject-invalid (bad-mac-address) (push) Skipped
E2E / reject-invalid (bad-repo-url) (push) Skipped
E2E / reject-invalid (bad-vlan-tag) (push) Skipped
E2E / reject-invalid (duplicate-gateway-addrs) (push) Skipped
E2E / reject-invalid (duplicate-node-names) (push) Skipped
E2E / reject-invalid (gateway-node-collision) (push) Skipped
E2E / reject-invalid (missing-dns-token) (push) Skipped
E2E / reject-invalid (nested-cidr-overlap) (push) Skipped
E2E / reject-invalid (node-addr-outside-cidr) (push) Skipped
E2E / reject-invalid (tunnel-without-dns) (push) Skipped
E2E / accept-valid (selfhosted) (push) Skipped
E2E / reject-invalid (missing-known-hosts) (push) Skipped
E2E / reject-invalid (missing-schematic) (push) Skipped
E2E / reject-invalid (partial-bgp) (push) Skipped
E2E / accept-valid (internal) (push) Skipped
E2E / reject-invalid (missing-external-gateway) (push) Skipped
E2E / reject-invalid (node-uses-gateway-addr) (push) Skipped
E2E / reject-invalid (non-canonical-cidr) (push) Skipped
E2E / accept-valid (private) (push) Skipped
E2E / accept-valid (single-node) (push) Skipped
E2E / reject-invalid (overlapping-cidrs) (push) Skipped
E2E / reject-invalid (reserved-node-name) (push) Skipped
E2E / reject-invalid (tiny-svc-cidr) (push) Skipped
E2E / validator-tests (push) Skipped
E2E / accept-valid (direct) (push) Skipped
E2E / accept-valid (multi-controller) (push) Skipped
E2E / accept-valid (no-webhook) (push) Skipped
E2E / accept-valid (public) (push) Skipped
Label Sync / Label Sync (push) Failing after 1m7s
E2E / reject-invalid (bad-bgp-asn) (push) Skipped
E2E / reject-invalid (bad-mac-address) (push) Skipped
E2E / reject-invalid (bad-repo-url) (push) Skipped
E2E / reject-invalid (bad-vlan-tag) (push) Skipped
E2E / reject-invalid (duplicate-gateway-addrs) (push) Skipped
E2E / reject-invalid (duplicate-node-names) (push) Skipped
E2E / reject-invalid (gateway-node-collision) (push) Skipped
E2E / reject-invalid (missing-dns-token) (push) Skipped
E2E / reject-invalid (nested-cidr-overlap) (push) Skipped
E2E / reject-invalid (node-addr-outside-cidr) (push) Skipped
E2E / reject-invalid (tunnel-without-dns) (push) Skipped
E2E / accept-valid (selfhosted) (push) Skipped
E2E / reject-invalid (missing-known-hosts) (push) Skipped
E2E / reject-invalid (missing-schematic) (push) Skipped
E2E / reject-invalid (partial-bgp) (push) Skipped
E2E / accept-valid (internal) (push) Skipped
E2E / reject-invalid (missing-external-gateway) (push) Skipped
E2E / reject-invalid (node-uses-gateway-addr) (push) Skipped
E2E / reject-invalid (non-canonical-cidr) (push) Skipped
E2E / accept-valid (private) (push) Skipped
E2E / accept-valid (single-node) (push) Skipped
E2E / reject-invalid (overlapping-cidrs) (push) Skipped
E2E / reject-invalid (reserved-node-name) (push) Skipped
E2E / reject-invalid (tiny-svc-cidr) (push) Skipped
E2E / validator-tests (push) Skipped
E2E / accept-valid (direct) (push) Skipped
E2E / accept-valid (multi-controller) (push) Skipped
E2E / accept-valid (no-webhook) (push) Skipped
E2E / accept-valid (public) (push) Skipped
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import base64
|
||||
import json
|
||||
import makejinja
|
||||
import re
|
||||
import validate
|
||||
|
||||
|
||||
# Return the stripped contents of file_path, rejecting a missing or empty file
|
||||
def _read_stripped(file_path: str) -> str:
|
||||
try:
|
||||
content = Path(file_path).read_text().strip()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"File not found: {file_path}") from None
|
||||
if not content:
|
||||
raise ValueError(f"{file_path} is empty")
|
||||
return content
|
||||
|
||||
|
||||
# Return the parsed contents of a JSON file
|
||||
def _read_json(file_path: str) -> dict[str, Any]:
|
||||
try:
|
||||
return json.loads(_read_stripped(file_path))
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Could not decode JSON file: {file_path}") from None
|
||||
|
||||
|
||||
# Return the age public or private key from age.key
|
||||
def age_key(key_type: str, file_path: str = 'age.key') -> str:
|
||||
file_content = _read_stripped(file_path)
|
||||
if key_type == 'public':
|
||||
# Matches both classic (age1...) and post-quantum (age1pq1...) recipients
|
||||
key_match = re.search(r"# public key: (age1[\w]+)", file_content)
|
||||
if not key_match:
|
||||
raise ValueError("Could not find public key in the age key file.")
|
||||
return key_match.group(1)
|
||||
elif key_type == 'private':
|
||||
# (?:PQ-)? matches post-quantum identities (AGE-SECRET-KEY-PQ-1...) as well as classic ones
|
||||
key_match = re.search(r"(AGE-SECRET-KEY-(?:PQ-)?1[\w]+)", file_content)
|
||||
if not key_match:
|
||||
raise ValueError("Could not find private key in the age key file.")
|
||||
return key_match.group(1)
|
||||
else:
|
||||
raise ValueError("Invalid key type. Use 'public' or 'private'.")
|
||||
|
||||
|
||||
# Return cloudflare tunnel fields from cloudflare-tunnel.json
|
||||
def cloudflare_tunnel_id(file_path: str = 'cloudflare-tunnel.json') -> str:
|
||||
data = _read_json(file_path)
|
||||
tunnel_id = data.get("TunnelID")
|
||||
if tunnel_id is None:
|
||||
raise KeyError(f"Missing 'TunnelID' key in {file_path}")
|
||||
if not tunnel_id:
|
||||
raise ValueError(f"'TunnelID' is empty in {file_path}")
|
||||
return tunnel_id
|
||||
|
||||
|
||||
# Return cloudflare tunnel fields from cloudflare-tunnel.json in TUNNEL_TOKEN format
|
||||
def cloudflare_tunnel_secret(file_path: str = 'cloudflare-tunnel.json') -> str:
|
||||
data = _read_json(file_path)
|
||||
for field in ("AccountTag", "TunnelID", "TunnelSecret"):
|
||||
if field not in data:
|
||||
raise KeyError(f"Missing '{field}' key in {file_path}")
|
||||
if not data[field]:
|
||||
raise ValueError(f"'{field}' is empty in {file_path}")
|
||||
transformed_data = {
|
||||
"a": data["AccountTag"],
|
||||
"t": data["TunnelID"],
|
||||
"s": data["TunnelSecret"]
|
||||
}
|
||||
json_string = json.dumps(transformed_data, separators=(',', ':'))
|
||||
return base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
|
||||
|
||||
|
||||
# Return the Flux deploy key from deploy.key
|
||||
def deploy_key(file_path: str = 'deploy.key') -> str:
|
||||
return _read_stripped(file_path)
|
||||
|
||||
|
||||
# Return the Flux webhook token from flux-webhook-token.txt
|
||||
def webhook_token(file_path: str = 'flux-webhook-token.txt') -> str:
|
||||
return _read_stripped(file_path)
|
||||
|
||||
|
||||
CONFIG_FILE = 'cluster.toml'
|
||||
|
||||
# SSH host keys as published by each provider. Must cover every host in
|
||||
# KNOWN_SSH_HOSTS in validate.py; any other host requires the user to set
|
||||
# repository.known_hosts in cluster.toml.
|
||||
KNOWN_HOSTS = {
|
||||
'github.com': (
|
||||
'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl\n'
|
||||
'github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\n'
|
||||
'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk='
|
||||
),
|
||||
'gitlab.com': (
|
||||
'gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf\n'
|
||||
'gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=\n'
|
||||
'gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9'
|
||||
),
|
||||
'codeberg.org': (
|
||||
'codeberg.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIVIC02vnjFyL+I4RHfvIGNtOgJMe769VTF1VR4EB3ZB\n'
|
||||
'codeberg.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL2pDxWr18SoiDJCGZ5LmxPygTlPu+cCKSkpqkvCyQzl5xmIMeKNdfdBpfbCGDPoZQghePzFZkKJNR/v9Win3Sc=\n'
|
||||
'codeberg.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8hZi7K1/2E2uBX8gwPRJAHvRAob+3Sn+y2hxiEhN0buv1igjYFTgFO2qQD8vLfU/HT/P/rqvEeTvaDfY1y/vcvQ8+YuUYyTwE2UaVU5aJv89y6PEZBYycaJCPdGIfZlLMmjilh/Sk8IWSEK6dQr+g686lu5cSWrFW60ixWpHpEVB26eRWin3lKYWSQGMwwKv4LwmW3ouqqs4Z4vsqRFqXJ/eCi3yhpT+nOjljXvZKiYTpYajqUC48IHAxTWugrKe1vXWOPxVXXMQEPsaIRc2hpK+v1LmfB7GnEGvF1UAKnEZbUuiD9PBEeD5a1MZQIzcoPWCrTxipEpuXQ5Tni4mN'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# makejinja adds import_paths (this directory) to sys.path, and both
|
||||
# makejinja and pydantic come from the uv project environment, so the
|
||||
# validator runs in-process.
|
||||
def validate_config() -> dict[str, Any]:
|
||||
try:
|
||||
return validate.load(CONFIG_FILE)
|
||||
except validate.ConfigError as e:
|
||||
raise RuntimeError(f"config validation failed:\n{e}") from None
|
||||
|
||||
|
||||
class Plugin(makejinja.plugin.Plugin):
|
||||
def __init__(self, data: dict[str, Any]):
|
||||
self._data = data
|
||||
|
||||
|
||||
def data(self) -> makejinja.plugin.Data:
|
||||
data = validate_config()
|
||||
if (
|
||||
data['ingress']['mode'] == 'cloudflare-tunnel'
|
||||
and not Path('cloudflare-tunnel.json').is_file()
|
||||
):
|
||||
raise RuntimeError('cloudflare-tunnel.json not found — see README')
|
||||
# The deploy key secret always pins every bundled provider host key
|
||||
# (entries are matched per-hostname, so unused ones are inert);
|
||||
# user-supplied entries for self-hosted git servers are appended.
|
||||
repository = data['repository']
|
||||
repository['known_hosts'] = '\n'.join(
|
||||
[*KNOWN_HOSTS.values(), repository['known_hosts']]
|
||||
).strip()
|
||||
return data
|
||||
|
||||
|
||||
def functions(self) -> makejinja.plugin.Functions:
|
||||
return [
|
||||
age_key,
|
||||
cloudflare_tunnel_id,
|
||||
cloudflare_tunnel_secret,
|
||||
deploy_key,
|
||||
webhook_token
|
||||
]
|
||||
Reference in New Issue
Block a user