new version
This commit is contained in:
1 parent
5edcd276ef
commit
498932a124
16 files changed
+1311
-384
No files matched your search
File renamed without changes.
@@ -0,0 +1,447 @@
|
||||
"""Réécrit une configuration Compose (sortie de `docker compose config --format json`)
|
||||
pour qu'elle puisse tourner en CI sur le même démon Docker que la production,
|
||||
sans jamais la toucher.
|
||||
|
||||
Exécuté dans un conteneur utilitaire lancé par compose-ci.sh :
|
||||
- stdin : configuration Compose résolue (JSON, contient des secrets : jamais affichée)
|
||||
- stdout : ligne 1 = nombre de services retenus
|
||||
ligne 2 = configuration réécrite (JSON sur une ligne)
|
||||
lignes suivantes = valeurs sensibles à masquer dans les journaux (base64)
|
||||
- stderr : rapport lisible (aucune valeur sensible)
|
||||
- /ci : copie du dépôt sur l'hôte (répertoire CI_HOST_DIR vu depuis ce conteneur)
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
|
||||
LOCAL_DIR = "/ci"
|
||||
|
||||
PROJECT_RE = re.compile(r"^ci-[a-z0-9]([a-z0-9-]*[a-z0-9])?-[0-9]+-[0-9]+$")
|
||||
|
||||
# Limites appliquées aux services qui n'en déclarent pas (identiques pour tous les dépôts).
|
||||
MEM_LIMIT = "1g"
|
||||
CPU_LIMIT = 1.0
|
||||
PIDS_LIMIT = 2048
|
||||
|
||||
# Labels lus par des composants de production (routage, découverte, mises à jour).
|
||||
DROPPED_LABEL_PREFIXES = (
|
||||
"traefik.",
|
||||
"homepage.",
|
||||
"com.centurylinklabs.watchtower.",
|
||||
"sablier.",
|
||||
)
|
||||
# Cible du test HTTP, déduite des labels Traefik avant leur suppression.
|
||||
HTTP_TARGET_LABEL = "tips-of-mine.ci.http"
|
||||
HTTP_PORT_RE = re.compile(r"^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$")
|
||||
HTTP_SCHEME_RE = re.compile(r"^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.scheme$")
|
||||
CI_LABELS = {
|
||||
"traefik.enable": "false",
|
||||
"com.centurylinklabs.watchtower.enable": "false",
|
||||
"tips-of-mine.ci": "true",
|
||||
}
|
||||
|
||||
# Capacités non isolées par les namespaces : elles agissent sur l'hôte.
|
||||
DANGEROUS_CAPS = {
|
||||
"ALL", "SYS_ADMIN", "SYS_MODULE", "SYS_RAWIO", "SYS_TIME", "SYS_BOOT",
|
||||
"SYSLOG", "DAC_READ_SEARCH", "MAC_ADMIN", "MAC_OVERRIDE", "BPF", "PERFMON",
|
||||
}
|
||||
HOST_NAMESPACE_KEYS = ("pid", "ipc", "uts", "userns_mode", "cgroup")
|
||||
|
||||
# Répertoires hôtes qui contiennent (ou sont parents de) sockets de démons.
|
||||
SOCKET_PARENTS = {"/", "/run", "/var", "/var/run"}
|
||||
|
||||
SENSITIVE_NAME_RE = re.compile(
|
||||
r"PASS|PWD|SECRET|TOKEN|KEY|CREDENTIAL|PRIVATE|AUTH|DSN|URI|URL|SALT|COOKIE",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
MASK_MIN_LENGTH = 6
|
||||
OOM_SCORE_ADJ = 500
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"ERREUR : {message}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def report(message):
|
||||
print(message, file=sys.stderr)
|
||||
|
||||
|
||||
def read_settings():
|
||||
project = os.environ.get("CI_PROJECT", "")
|
||||
workspace = os.environ.get("CI_WORKSPACE", "")
|
||||
host_dir = os.environ.get("CI_HOST_DIR", "")
|
||||
|
||||
if not PROJECT_RE.match(project):
|
||||
fail(f"nom de projet refusé : {project!r}")
|
||||
if not workspace.startswith("/"):
|
||||
fail("CI_WORKSPACE doit être un chemin absolu")
|
||||
if host_dir != f"/var/tmp/gitea-ci/{project}":
|
||||
fail(f"répertoire hôte refusé : {host_dir!r}")
|
||||
|
||||
return {
|
||||
"project": project,
|
||||
"workspace": posixpath.normpath(workspace),
|
||||
"host_dir": host_dir,
|
||||
"mem": MEM_LIMIT,
|
||||
"cpus": CPU_LIMIT,
|
||||
"pids": PIDS_LIMIT,
|
||||
}
|
||||
|
||||
|
||||
def is_under(path, root):
|
||||
path = posixpath.normpath(path)
|
||||
return path == root or path.startswith(root + "/")
|
||||
|
||||
|
||||
def relative_to_workspace(path, settings):
|
||||
return posixpath.relpath(posixpath.normpath(path), settings["workspace"])
|
||||
|
||||
|
||||
def host_path_problem(path, read_only):
|
||||
"""Raison d'exclusion d'un montage hôte situé hors du dépôt, ou None."""
|
||||
path = posixpath.normpath(path)
|
||||
if path.endswith(".sock") or "docker.sock" in path:
|
||||
return "socket de l'hôte"
|
||||
if path in SOCKET_PARENTS or path.startswith(("/run/", "/var/run/")):
|
||||
return "répertoire de sockets de l'hôte"
|
||||
if path == "/dev" or path.startswith("/dev/"):
|
||||
return "périphérique de l'hôte"
|
||||
if not read_only:
|
||||
return "montage de l'hôte en écriture"
|
||||
return None
|
||||
|
||||
|
||||
def copied_path_problem(path, settings):
|
||||
"""Refuse un chemin du dépôt qui sort de la copie via un lien symbolique."""
|
||||
rel = relative_to_workspace(path, settings)
|
||||
local = posixpath.normpath(posixpath.join(LOCAL_DIR, rel))
|
||||
if not is_under(os.path.realpath(local), LOCAL_DIR):
|
||||
return f"lien symbolique sortant du dépôt ({rel})"
|
||||
return None
|
||||
|
||||
|
||||
def file_resource_problem(resource, settings):
|
||||
"""Contrôle un secret ou une config de premier niveau déclaré par fichier."""
|
||||
source = (resource or {}).get("file")
|
||||
if not source:
|
||||
return None
|
||||
if is_under(source, settings["workspace"]):
|
||||
return copied_path_problem(source, settings)
|
||||
return host_path_problem(source, read_only=True)
|
||||
|
||||
|
||||
def service_problems(svc, settings, unsafe_secrets, unsafe_configs):
|
||||
reasons = []
|
||||
if svc.get("privileged"):
|
||||
reasons.append("privileged")
|
||||
for key in HOST_NAMESPACE_KEYS:
|
||||
if svc.get(key) == "host":
|
||||
reasons.append(f"{key}: host")
|
||||
network_mode = svc.get("network_mode") or ""
|
||||
if network_mode == "host" or network_mode.startswith("container:"):
|
||||
reasons.append(f"network_mode: {network_mode}")
|
||||
|
||||
reservations = ((svc.get("deploy") or {}).get("resources") or {}).get("reservations") or {}
|
||||
if svc.get("devices") or svc.get("device_cgroup_rules") or svc.get("gpus") or reservations.get("devices"):
|
||||
reasons.append("accès à des périphériques")
|
||||
|
||||
caps = {str(cap).upper().removeprefix("CAP_") for cap in svc.get("cap_add") or []}
|
||||
if caps & DANGEROUS_CAPS:
|
||||
reasons.append("cap_add " + ",".join(sorted(caps & DANGEROUS_CAPS)))
|
||||
|
||||
for entry in svc.get("volumes_from") or []:
|
||||
if str(entry).startswith("container:"):
|
||||
reasons.append("volumes_from vers un conteneur externe")
|
||||
|
||||
for volume in svc.get("volumes") or []:
|
||||
if volume.get("type") != "bind":
|
||||
continue
|
||||
source = volume.get("source") or ""
|
||||
if is_under(source, settings["workspace"]):
|
||||
problem = copied_path_problem(source, settings)
|
||||
else:
|
||||
problem = host_path_problem(source, bool(volume.get("read_only")))
|
||||
if problem:
|
||||
problem = f"{problem} ({source})"
|
||||
if problem:
|
||||
reasons.append(problem)
|
||||
|
||||
for ref in svc.get("secrets") or []:
|
||||
if ref.get("source") in unsafe_secrets:
|
||||
reasons.append(f"secret {ref.get('source')} non isolable")
|
||||
for ref in svc.get("configs") or []:
|
||||
if ref.get("source") in unsafe_configs:
|
||||
reasons.append(f"config {ref.get('source')} non isolable")
|
||||
return reasons
|
||||
|
||||
|
||||
def dependency_targets(svc):
|
||||
"""Services dont celui-ci ne peut pas se passer (réseau ou volumes partagés)."""
|
||||
targets = []
|
||||
network_mode = svc.get("network_mode") or ""
|
||||
if network_mode.startswith("service:"):
|
||||
targets.append(network_mode.removeprefix("service:"))
|
||||
for entry in svc.get("volumes_from") or []:
|
||||
entry = str(entry)
|
||||
if not entry.startswith("container:"):
|
||||
targets.append(entry.removeprefix("service:").split(":")[0])
|
||||
return targets
|
||||
|
||||
|
||||
def select_services(cfg, settings):
|
||||
services = cfg.get("services") or {}
|
||||
unsafe_secrets = {
|
||||
name for name, res in (cfg.get("secrets") or {}).items()
|
||||
if file_resource_problem(res, settings)
|
||||
}
|
||||
unsafe_configs = {
|
||||
name for name, res in (cfg.get("configs") or {}).items()
|
||||
if file_resource_problem(res, settings)
|
||||
}
|
||||
|
||||
excluded = {}
|
||||
for name, svc in services.items():
|
||||
reasons = service_problems(svc, settings, unsafe_secrets, unsafe_configs)
|
||||
if reasons:
|
||||
excluded[name] = reasons
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for name, svc in services.items():
|
||||
if name in excluded:
|
||||
continue
|
||||
missing = sorted({t for t in dependency_targets(svc) if t in excluded})
|
||||
if missing:
|
||||
excluded[name] = [f"dépend du service exclu {', '.join(missing)}"]
|
||||
changed = True
|
||||
return excluded
|
||||
|
||||
|
||||
def collect_masks(services):
|
||||
masks = set()
|
||||
for svc in services.values():
|
||||
environment = svc.get("environment") or {}
|
||||
if isinstance(environment, list):
|
||||
environment = dict(item.split("=", 1) for item in environment if "=" in item)
|
||||
for key, value in environment.items():
|
||||
if value is None or not SENSITIVE_NAME_RE.search(key):
|
||||
continue
|
||||
value = str(value)
|
||||
# `compose config` échappe les $ en $$ ; le conteneur voit la forme simple.
|
||||
for variant in {value, value.replace("$$", "$")}:
|
||||
if len(variant) >= MASK_MIN_LENGTH:
|
||||
masks.add(variant)
|
||||
return masks
|
||||
|
||||
|
||||
def service_owner(svc):
|
||||
"""uid/gid numériques du `user:` du service, ou None s'ils ne sont pas numériques."""
|
||||
user = str(svc.get("user") or "")
|
||||
uid, _, gid = user.partition(":")
|
||||
if not uid.isdigit() or (gid and not gid.isdigit()):
|
||||
return None
|
||||
return int(uid), int(gid) if gid else int(uid)
|
||||
|
||||
|
||||
def chown_tree(local, owner):
|
||||
"""Attribue une copie du dépôt à l'utilisateur du service, sans suivre les liens."""
|
||||
uid, gid = owner
|
||||
os.lchown(local, uid, gid)
|
||||
if os.path.isdir(local) and not os.path.islink(local):
|
||||
for root, dirs, files in os.walk(local, followlinks=False):
|
||||
for entry in dirs + files:
|
||||
os.lchown(posixpath.join(root, entry), uid, gid)
|
||||
|
||||
|
||||
def relocate_source(source, settings, create_missing, owner=None):
|
||||
rel = relative_to_workspace(source, settings)
|
||||
local = posixpath.normpath(posixpath.join(LOCAL_DIR, rel))
|
||||
if create_missing and not os.path.lexists(local):
|
||||
os.makedirs(local, exist_ok=True)
|
||||
os.chmod(local, 0o777)
|
||||
if owner and rel != ".":
|
||||
chown_tree(local, owner)
|
||||
if rel == ".":
|
||||
return settings["host_dir"]
|
||||
return posixpath.join(settings["host_dir"], rel)
|
||||
|
||||
|
||||
def http_target(labels):
|
||||
"""Cibles HTTP d'un service exposé par Traefik : "http:8200,https:8443", "auto" ou None."""
|
||||
if not any(key.startswith("traefik.http.") for key in labels):
|
||||
return None
|
||||
if str(labels.get("traefik.enable", "")).strip().lower() == "false":
|
||||
return None
|
||||
ports, schemes = {}, {}
|
||||
for key, value in labels.items():
|
||||
match = HTTP_PORT_RE.match(key)
|
||||
if match:
|
||||
ports[match.group(1)] = str(value).strip()
|
||||
match = HTTP_SCHEME_RE.match(key)
|
||||
if match:
|
||||
schemes[match.group(1)] = str(value).strip().lower()
|
||||
targets = set()
|
||||
for name, port in ports.items():
|
||||
if port.isdigit():
|
||||
scheme = "https" if schemes.get(name) == "https" else "http"
|
||||
targets.add(f"{scheme}:{port}")
|
||||
return ",".join(sorted(targets)) if targets else "auto"
|
||||
|
||||
|
||||
def rewrite_labels(labels, project):
|
||||
if isinstance(labels, list):
|
||||
labels = dict(item.split("=", 1) if "=" in item else (item, "") for item in labels)
|
||||
labels = labels or {}
|
||||
target = http_target(labels)
|
||||
kept = {k: v for k, v in labels.items() if not k.startswith(DROPPED_LABEL_PREFIXES)}
|
||||
kept.update(CI_LABELS)
|
||||
kept["tips-of-mine.ci.project"] = project
|
||||
if target:
|
||||
kept[HTTP_TARGET_LABEL] = target
|
||||
return kept
|
||||
|
||||
|
||||
def apply_limits(svc, settings):
|
||||
deploy = svc.get("deploy") or {}
|
||||
deploy.pop("restart_policy", None)
|
||||
limits = (deploy.get("resources") or {}).get("limits")
|
||||
|
||||
if limits is not None:
|
||||
# Compose refuse deux valeurs distinctes (ex. cpus et deploy...limits.cpus) :
|
||||
# on complète le bloc déjà déclaré par le service.
|
||||
if not svc.get("mem_limit") and not limits.get("memory"):
|
||||
limits["memory"] = settings["mem"]
|
||||
if not svc.get("cpus") and not limits.get("cpus"):
|
||||
limits["cpus"] = str(settings["cpus"])
|
||||
if not svc.get("pids_limit") and not limits.get("pids"):
|
||||
limits["pids"] = settings["pids"]
|
||||
else:
|
||||
svc.setdefault("mem_limit", settings["mem"])
|
||||
svc.setdefault("cpus", settings["cpus"])
|
||||
svc.setdefault("pids_limit", settings["pids"])
|
||||
|
||||
# Pas de swap pour la CI : le swap de l'hôte sert à la production.
|
||||
memory = svc.get("mem_limit") or (limits or {}).get("memory")
|
||||
if memory and not svc.get("memswap_limit"):
|
||||
svc["memswap_limit"] = memory
|
||||
svc["oom_score_adj"] = max(int(svc.get("oom_score_adj") or 0), OOM_SCORE_ADJ)
|
||||
|
||||
if deploy:
|
||||
svc["deploy"] = deploy
|
||||
else:
|
||||
svc.pop("deploy", None)
|
||||
|
||||
|
||||
def rewrite_service(name, svc, excluded, settings):
|
||||
for key in ("container_name", "ports", "logging", "mac_address", "external_links"):
|
||||
svc.pop(key, None)
|
||||
if "build" in svc:
|
||||
# Image nommée <projet>-<service> : aucun tag de production n'est écrasé.
|
||||
svc.pop("image", None)
|
||||
elif svc.get("pull_policy") not in (None, "missing", "if_not_present", "never"):
|
||||
svc["pull_policy"] = "missing"
|
||||
svc["restart"] = "no"
|
||||
svc["labels"] = rewrite_labels(svc.get("labels"), settings["project"])
|
||||
apply_limits(svc, settings)
|
||||
|
||||
networks = svc.get("networks")
|
||||
if isinstance(networks, dict):
|
||||
for attachment in networks.values():
|
||||
if isinstance(attachment, dict):
|
||||
for key in ("ipv4_address", "ipv6_address", "link_local_ips", "mac_address"):
|
||||
attachment.pop(key, None)
|
||||
|
||||
depends_on = svc.get("depends_on")
|
||||
if isinstance(depends_on, dict):
|
||||
for target in [t for t in depends_on if t in excluded]:
|
||||
report(f" - {name} : dépendance vers {target} retirée (service exclu)")
|
||||
del depends_on[target]
|
||||
if not depends_on:
|
||||
svc.pop("depends_on")
|
||||
if svc.get("links"):
|
||||
svc["links"] = [l for l in svc["links"] if str(l).split(":")[0] not in excluded]
|
||||
if not svc["links"]:
|
||||
svc.pop("links")
|
||||
|
||||
# Les montages en écriture appartiennent à l'utilisateur du service, comme en
|
||||
# production : certaines images refusent de démarrer sinon (chown impossible).
|
||||
owner = service_owner(svc)
|
||||
relocated = 0
|
||||
for volume in svc.get("volumes") or []:
|
||||
if volume.get("type") == "bind" and is_under(volume.get("source") or "", settings["workspace"]):
|
||||
writable_owner = None if volume.get("read_only") else owner
|
||||
volume["source"] = relocate_source(
|
||||
volume["source"], settings, create_missing=True, owner=writable_owner
|
||||
)
|
||||
relocated += 1
|
||||
return relocated
|
||||
|
||||
|
||||
def rewrite_top_level(cfg, settings):
|
||||
project = settings["project"]
|
||||
for key, network in list((cfg.get("networks") or {}).items()):
|
||||
network = network or {}
|
||||
for field in ("external", "driver", "driver_opts", "ipam", "enable_ipv6"):
|
||||
network.pop(field, None)
|
||||
network["name"] = f"{project}_{key}"
|
||||
cfg["networks"][key] = network
|
||||
|
||||
for key, volume in list((cfg.get("volumes") or {}).items()):
|
||||
volume = volume or {}
|
||||
for field in ("external", "driver", "driver_opts"):
|
||||
volume.pop(field, None)
|
||||
volume["name"] = f"{project}_{key}"
|
||||
cfg["volumes"][key] = volume
|
||||
|
||||
for section in ("secrets", "configs"):
|
||||
for resource in (cfg.get(section) or {}).values():
|
||||
source = (resource or {}).get("file")
|
||||
if source and is_under(source, settings["workspace"]):
|
||||
resource["file"] = relocate_source(source, settings, create_missing=False)
|
||||
|
||||
cfg["name"] = project
|
||||
|
||||
|
||||
def main():
|
||||
settings = read_settings()
|
||||
try:
|
||||
cfg = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
fail("configuration Compose illisible")
|
||||
|
||||
services = cfg.get("services") or {}
|
||||
if not services:
|
||||
fail("aucun service dans la configuration Compose")
|
||||
|
||||
masks = collect_masks(services)
|
||||
excluded = select_services(cfg, settings)
|
||||
for name in excluded:
|
||||
del services[name]
|
||||
|
||||
report(f"Projet de CI : {settings['project']}")
|
||||
report(f"Services testés : {', '.join(sorted(services)) or 'aucun'}")
|
||||
if excluded:
|
||||
report("Services exclus :")
|
||||
for name in sorted(excluded):
|
||||
report(f" - {name} : {'; '.join(excluded[name])}")
|
||||
|
||||
relocated = sum(rewrite_service(n, s, excluded, settings) for n, s in services.items())
|
||||
rewrite_top_level(cfg, settings)
|
||||
cfg["services"] = services
|
||||
report(f"Montages relocalisés : {relocated}")
|
||||
|
||||
out = sys.stdout
|
||||
out.write(f"{len(services)}\n")
|
||||
out.write(json.dumps(cfg, separators=(",", ":")) + "\n")
|
||||
for value in sorted(masks):
|
||||
out.write(base64.b64encode(value.encode()).decode() + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,478 @@
|
||||
#!/usr/bin/env bash
|
||||
# Vérification d'une stack Docker Compose en CI, sur le démon Docker de l'hôte,
|
||||
# sans aucune interaction avec les conteneurs, réseaux et volumes de production.
|
||||
#
|
||||
# Usage : compose-ci.sh prepare|up|verify|logs|cleanup
|
||||
#
|
||||
# Aucune variable ni aucun secret à configurer : les réglages sont fixés ci-dessous
|
||||
# et identiques pour tous les dépôts.
|
||||
# Fichier facultatif dans le dépôt : .env.example (utilisé si .env est absent),
|
||||
# nécessaire seulement si le compose utilise des ${VARIABLE} sans valeur par défaut.
|
||||
# Un dépôt sans fichier Compose (ex. le dépôt Template) ne teste rien et reste au vert.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly HOST_ROOT="/var/tmp/gitea-ci"
|
||||
readonly HELPER_IMAGE="python:3.13-alpine3.22"
|
||||
readonly CI_MARKER_LABEL="tips-of-mine.ci"
|
||||
readonly STABILIZATION_SECONDS=15
|
||||
readonly POLL_SECONDS=5
|
||||
readonly WAIT_TIMEOUT_SECONDS=300
|
||||
readonly HTTP_TIMEOUT_SECONDS=300
|
||||
readonly HTTP_TARGET_LABEL="tips-of-mine.ci.http"
|
||||
readonly MIN_AVAILABLE_MB=3072
|
||||
readonly LOG_LINES=50
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SCRIPT_DIR
|
||||
readonly WORK_DIR="${RUNNER_TEMP:-/tmp}/compose-ci"
|
||||
readonly CI_FILE="${WORK_DIR}/compose.ci.json"
|
||||
readonly COUNT_FILE="${WORK_DIR}/services.count"
|
||||
readonly MASK_FILE="${WORK_DIR}/mask.b64"
|
||||
readonly STARTED_FILE="${WORK_DIR}/started.epoch"
|
||||
readonly SENSITIVE_NAME_RE='PASS|PWD|SECRET|TOKEN|KEY|CREDENTIAL|PRIVATE|AUTH|DSN|URI|URL|SALT|COOKIE'
|
||||
readonly MASK_MIN_LENGTH=6
|
||||
|
||||
# Interroge une URL jusqu'à obtenir une réponse HTTP < 500 ou jusqu'à l'échéance.
|
||||
# Les redirections ne sont pas suivies : une réponse 3xx prouve que l'application répond.
|
||||
PROBE_PY="$(cat <<'PY'
|
||||
import os, ssl, sys, time, urllib.error, urllib.request
|
||||
|
||||
url = os.environ["PROBE_URL"]
|
||||
start = float(os.environ["PROBE_START"])
|
||||
deadline = float(os.environ["PROBE_DEADLINE"])
|
||||
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
opener = urllib.request.build_opener(
|
||||
NoRedirect, urllib.request.HTTPSHandler(context=ssl._create_unverified_context())
|
||||
)
|
||||
last = "aucune réponse"
|
||||
while True:
|
||||
code = None
|
||||
try:
|
||||
with opener.open(url, timeout=5) as response:
|
||||
code = response.status
|
||||
except urllib.error.HTTPError as error:
|
||||
code = error.code
|
||||
except Exception as error:
|
||||
last = str(getattr(error, "reason", "") or type(error).__name__)
|
||||
if code is not None:
|
||||
if code < 500:
|
||||
print(f"HTTP {code} disponible en {time.time() - start:.0f} s")
|
||||
sys.exit(0)
|
||||
last = f"HTTP {code}"
|
||||
if time.time() >= deadline:
|
||||
print(f"ÉCHEC ({last}) indisponible après {time.time() - start:.0f} s")
|
||||
sys.exit(1)
|
||||
time.sleep(2)
|
||||
PY
|
||||
)"
|
||||
readonly PROBE_PY
|
||||
|
||||
PROJECT=""
|
||||
SERVICE_COUNT=0
|
||||
ENV_FILE=""
|
||||
ENV_SOURCE=""
|
||||
|
||||
die() {
|
||||
echo "ERREUR : $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
project_name() {
|
||||
local repo run_id attempt name
|
||||
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY absent}"
|
||||
repo="${repo##*/}"
|
||||
repo="$(printf '%s' "${repo,,}" | tr -c 'a-z0-9' '-' | tr -s '-')"
|
||||
repo="${repo#-}"
|
||||
repo="${repo:0:40}"
|
||||
repo="${repo%-}"
|
||||
run_id="${GITHUB_RUN_ID:?GITHUB_RUN_ID absent}"
|
||||
attempt="${GITHUB_RUN_ATTEMPT:-1}"
|
||||
name="ci-${repo}-${run_id}-${attempt}"
|
||||
[[ "$name" =~ ^ci-[a-z0-9]([a-z0-9-]*[a-z0-9])?-[0-9]+-[0-9]+$ ]] \
|
||||
|| die "nom de projet de CI invalide : ${name}"
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
compose_file() {
|
||||
local candidate
|
||||
for candidate in docker-compose.yml docker-compose.yaml compose.yaml compose.yml; do
|
||||
if [[ -f "${GITHUB_WORKSPACE}/${candidate}" ]]; then
|
||||
printf '%s' "$candidate"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
resolve_env_file() {
|
||||
if [[ -f "${GITHUB_WORKSPACE}/.env" ]]; then
|
||||
ENV_FILE="${GITHUB_WORKSPACE}/.env"
|
||||
ENV_SOURCE=".env du dépôt"
|
||||
elif [[ -f "${GITHUB_WORKSPACE}/.env.example" ]]; then
|
||||
ENV_FILE="${GITHUB_WORKSPACE}/.env.example"
|
||||
ENV_SOURCE=".env.example du dépôt"
|
||||
else
|
||||
ENV_FILE=""
|
||||
ENV_SOURCE="aucun"
|
||||
fi
|
||||
}
|
||||
|
||||
# Ajoute au masquage les valeurs des variables sensibles du fichier d'environnement.
|
||||
mask_env_file() {
|
||||
local line name value
|
||||
[[ -n "$ENV_FILE" ]] || return 0
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
[[ "$line" =~ ^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]] || continue
|
||||
name="${BASH_REMATCH[2]}"
|
||||
value="${BASH_REMATCH[3]}"
|
||||
[[ "$name" =~ $SENSITIVE_NAME_RE ]] || continue
|
||||
value="${value%$'\r'}"
|
||||
value=${value#[\"\']}
|
||||
value=${value%[\"\']}
|
||||
(( ${#value} >= MASK_MIN_LENGTH )) || continue
|
||||
printf '%s' "$value" | base64 -w0 >> "$MASK_FILE"
|
||||
printf '\n' >> "$MASK_FILE"
|
||||
done < "$ENV_FILE"
|
||||
}
|
||||
|
||||
compose() {
|
||||
docker compose --ansi never \
|
||||
--project-name "$PROJECT" \
|
||||
--project-directory "$GITHUB_WORKSPACE" \
|
||||
-f "$CI_FILE" "$@"
|
||||
}
|
||||
|
||||
helper() {
|
||||
docker run --rm --network none "$@"
|
||||
}
|
||||
|
||||
helper_on() {
|
||||
local network="$1"
|
||||
shift
|
||||
[[ "$network" == "${PROJECT}_"* ]] || die "réseau hors du projet de CI refusé : ${network}"
|
||||
docker run --rm --network "$network" "$@"
|
||||
}
|
||||
|
||||
load_state() {
|
||||
PROJECT="$(project_name)"
|
||||
[[ -f "$COUNT_FILE" ]] \
|
||||
|| die "configuration de CI absente : l'étape de préparation n'a pas abouti"
|
||||
SERVICE_COUNT="$(<"$COUNT_FILE")"
|
||||
[[ "$SERVICE_COUNT" =~ ^[0-9]+$ ]] || die "nombre de services illisible"
|
||||
if (( SERVICE_COUNT > 0 )) && [[ ! -f "$CI_FILE" ]]; then
|
||||
die "configuration de CI absente : l'étape de préparation n'a pas abouti"
|
||||
fi
|
||||
}
|
||||
|
||||
check_memory() {
|
||||
local min="$MIN_AVAILABLE_MB" available
|
||||
available="$(awk '/^MemAvailable:/ { print int($2 / 1024) }' /proc/meminfo)"
|
||||
[[ "$available" =~ ^[0-9]+$ ]] || die "mémoire disponible illisible"
|
||||
echo "Mémoire disponible sur l'hôte : ${available} Mio (seuil : ${min} Mio)"
|
||||
(( available >= min )) \
|
||||
|| die "mémoire insuffisante sur l'hôte : démarrage annulé pour protéger la production"
|
||||
}
|
||||
|
||||
assert_isolated() {
|
||||
local key
|
||||
for key in '"container_name":' '"ports":' '"external":'; do
|
||||
if grep -qF "$key" "$CI_FILE"; then
|
||||
die "la configuration de CI contient encore ${key} : arrêt par sécurité"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_prepare() {
|
||||
local file raw errors unset_vars
|
||||
local -a env_args=()
|
||||
PROJECT="$(project_name)"
|
||||
install -d -m 0700 "$WORK_DIR"
|
||||
file="$(compose_file)"
|
||||
if [[ -z "$file" ]]; then
|
||||
echo "Aucun fichier Compose à la racine du dépôt : rien à tester"
|
||||
echo 0 > "$COUNT_FILE"
|
||||
return 0
|
||||
fi
|
||||
echo "Fichier Compose : ${file}"
|
||||
check_memory
|
||||
|
||||
resolve_env_file
|
||||
echo "Variables d'environnement : ${ENV_SOURCE}"
|
||||
if [[ -n "$ENV_FILE" ]]; then
|
||||
env_args=(--env-file "$ENV_FILE")
|
||||
fi
|
||||
|
||||
echo "Copie du dépôt vers ${HOST_ROOT}/${PROJECT} sur l'hôte"
|
||||
tar -C "$GITHUB_WORKSPACE" --exclude=./.git -cf - . \
|
||||
| helper -i -v "${HOST_ROOT}/${PROJECT}:/dst" "$HELPER_IMAGE" tar -C /dst -xf -
|
||||
|
||||
echo "Réécriture de la configuration Compose"
|
||||
raw="${WORK_DIR}/rewrite.out"
|
||||
errors="${WORK_DIR}/config.err"
|
||||
if ! docker compose --ansi never \
|
||||
--project-name "$PROJECT" \
|
||||
--project-directory "$GITHUB_WORKSPACE" \
|
||||
"${env_args[@]}" \
|
||||
-f "${GITHUB_WORKSPACE}/${file}" \
|
||||
config --format json 2> "$errors" \
|
||||
| helper -i \
|
||||
-e CI_PROJECT="$PROJECT" \
|
||||
-e CI_WORKSPACE="$GITHUB_WORKSPACE" \
|
||||
-e CI_HOST_DIR="${HOST_ROOT}/${PROJECT}" \
|
||||
-v "${HOST_ROOT}/${PROJECT}:/ci" \
|
||||
"$HELPER_IMAGE" python3 -c "$(<"${SCRIPT_DIR}/compose-ci-rewrite.py")" \
|
||||
> "$raw"; then
|
||||
cat "$errors" >&2
|
||||
die "la configuration Compose n'a pas pu être résolue"
|
||||
fi
|
||||
|
||||
unset_vars="$(sed -nE 's/.*The \\?"([A-Za-z_][A-Za-z0-9_]*)\\?" variable is not set.*/\1/p' "$errors" | sort -u | paste -sd ' ' -)"
|
||||
if [[ -n "$unset_vars" ]]; then
|
||||
die "variables non définies (${ENV_SOURCE}) : ${unset_vars}"
|
||||
fi
|
||||
if [[ -s "$errors" ]]; then
|
||||
cat "$errors" >&2
|
||||
fi
|
||||
|
||||
sed -n '1p' "$raw" > "$COUNT_FILE"
|
||||
sed -n '2p' "$raw" > "$CI_FILE"
|
||||
sed -n '3,$p' "$raw" > "$MASK_FILE"
|
||||
rm -f "$raw"
|
||||
chmod 0600 "$CI_FILE" "$MASK_FILE"
|
||||
mask_env_file
|
||||
|
||||
load_state
|
||||
assert_isolated
|
||||
compose config --quiet
|
||||
echo "Configuration de CI validée (${SERVICE_COUNT} service(s))"
|
||||
}
|
||||
|
||||
cmd_up() {
|
||||
load_state
|
||||
if (( SERVICE_COUNT == 0 )); then
|
||||
echo "Aucun service testable dans ce dépôt : rien à démarrer"
|
||||
return 0
|
||||
fi
|
||||
compose up --detach --build --quiet-pull
|
||||
date +%s > "$STARTED_FILE"
|
||||
}
|
||||
|
||||
evaluate_state() {
|
||||
local ids id name status code health verdict
|
||||
STATE_PENDING=0
|
||||
STATE_FAILED=0
|
||||
STATE_TABLE="$(printf '%-50s %-11s %-5s %-10s %s' CONTENEUR ETAT CODE SANTE VERDICT)"$'\n'
|
||||
|
||||
mapfile -t ids < <(compose ps --all --quiet | grep -v '^$' || true)
|
||||
if (( ${#ids[@]} == 0 )); then
|
||||
STATE_FAILED=1
|
||||
STATE_TABLE+="aucun conteneur créé"$'\n'
|
||||
return
|
||||
fi
|
||||
|
||||
for id in "${ids[@]}"; do
|
||||
IFS='|' read -r name status code health < <(
|
||||
docker inspect --format \
|
||||
'{{.Name}}|{{.State.Status}}|{{.State.ExitCode}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}' \
|
||||
"$id"
|
||||
)
|
||||
case "${status}:${health}" in
|
||||
running:- | running:healthy)
|
||||
verdict="OK" ;;
|
||||
running:starting | running:unhealthy | created:* | restarting:*)
|
||||
verdict="ATTENTE"; STATE_PENDING=1 ;;
|
||||
exited:*)
|
||||
if [[ "$code" == "0" ]]; then
|
||||
verdict="TERMINÉ"
|
||||
else
|
||||
verdict="ÉCHEC"; STATE_FAILED=1
|
||||
fi ;;
|
||||
*)
|
||||
verdict="ÉCHEC"; STATE_FAILED=1 ;;
|
||||
esac
|
||||
STATE_TABLE+="$(printf '%-50s %-11s %-5s %-10s %s' "${name#/}" "$status" "$code" "$health" "$verdict")"$'\n'
|
||||
done
|
||||
}
|
||||
|
||||
single_exposed_port() {
|
||||
local id="$1" entry
|
||||
local -a ports=()
|
||||
for entry in $(docker inspect --format '{{range $p, $v := .Config.ExposedPorts}}{{$p}} {{end}}' "$id"); do
|
||||
[[ "$entry" == */tcp ]] && ports+=("${entry%/tcp}")
|
||||
done
|
||||
(( ${#ports[@]} == 1 )) && printf '%s' "${ports[0]}"
|
||||
}
|
||||
|
||||
probe() {
|
||||
local network="$1" url="$2" started="$3" result status=0
|
||||
result="$(helper_on "$network" \
|
||||
-e PROBE_URL="$url" \
|
||||
-e PROBE_START="$started" \
|
||||
-e PROBE_DEADLINE="$(( started + HTTP_TIMEOUT_SECONDS ))" \
|
||||
"$HELPER_IMAGE" python3 -c "$PROBE_PY" 2>&1)" || status=$?
|
||||
printf '%-70s %s\n' "$url" "$result"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
http_checks() {
|
||||
local started ids id name spec networks network port target scheme tested=0 status=0
|
||||
local -a targets=()
|
||||
[[ -f "$STARTED_FILE" ]] || die "heure de démarrage inconnue : l'étape de démarrage n'a pas abouti"
|
||||
started="$(<"$STARTED_FILE")"
|
||||
|
||||
echo
|
||||
echo "Test HTTP (délai maximal : ${HTTP_TIMEOUT_SECONDS} s après le démarrage)"
|
||||
mapfile -t ids < <(compose ps --quiet | grep -v '^$' || true)
|
||||
for id in "${ids[@]}"; do
|
||||
IFS='|' read -r name spec networks < <(
|
||||
docker inspect --format \
|
||||
"{{.Name}}|{{index .Config.Labels \"${HTTP_TARGET_LABEL}\"}}|{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}} {{end}}" \
|
||||
"$id"
|
||||
)
|
||||
[[ -n "$spec" ]] || continue
|
||||
name="${name#/}"
|
||||
network="${networks%% *}"
|
||||
if [[ -z "$network" ]]; then
|
||||
echo "${name} : aucun réseau propre, test HTTP ignoré"
|
||||
continue
|
||||
fi
|
||||
if [[ "$spec" == "auto" ]]; then
|
||||
port="$(single_exposed_port "$id" || true)"
|
||||
if [[ -z "$port" ]]; then
|
||||
echo "${name} : port HTTP indéterminable (aucun label de port Traefik, ports exposés multiples ou absents), test HTTP ignoré"
|
||||
continue
|
||||
fi
|
||||
spec="http:${port}"
|
||||
fi
|
||||
IFS=',' read -ra targets <<<"$spec"
|
||||
for target in "${targets[@]}"; do
|
||||
scheme="${target%%:*}"
|
||||
port="${target##*:}"
|
||||
tested=1
|
||||
probe "$network" "${scheme}://${name}:${port}/" "$started" || status=1
|
||||
done
|
||||
done
|
||||
|
||||
if (( ! tested )); then
|
||||
echo "Aucun service exposé par Traefik : test HTTP non applicable"
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
cmd_verify() {
|
||||
local timeout deadline stable=0
|
||||
load_state
|
||||
if (( SERVICE_COUNT == 0 )); then
|
||||
echo "Rien à vérifier"
|
||||
return 0
|
||||
fi
|
||||
|
||||
timeout="$WAIT_TIMEOUT_SECONDS"
|
||||
deadline=$(( SECONDS + timeout ))
|
||||
|
||||
while :; do
|
||||
evaluate_state
|
||||
if (( STATE_FAILED )); then
|
||||
printf '%s' "$STATE_TABLE"
|
||||
die "au moins un conteneur est en échec"
|
||||
fi
|
||||
if (( ! STATE_PENDING )); then
|
||||
if (( stable )); then
|
||||
printf '%s' "$STATE_TABLE"
|
||||
echo "Tous les conteneurs sont démarrés et stables"
|
||||
http_checks || die "au moins un service web ne répond pas dans le délai"
|
||||
return 0
|
||||
fi
|
||||
stable=1
|
||||
sleep "$STABILIZATION_SECONDS"
|
||||
continue
|
||||
fi
|
||||
stable=0
|
||||
if (( SECONDS >= deadline )); then
|
||||
printf '%s' "$STATE_TABLE"
|
||||
die "délai de ${timeout} s dépassé avant que tous les conteneurs soient prêts"
|
||||
fi
|
||||
sleep "$POLL_SECONDS"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
local logs encoded secret
|
||||
if [[ ! -f "$CI_FILE" ]]; then
|
||||
echo "Pas de configuration de CI : aucun journal à afficher"
|
||||
return 0
|
||||
fi
|
||||
load_state
|
||||
|
||||
compose ps --all --format 'table {{.Service}}\t{{.State}}\t{{.Status}}' || true
|
||||
logs="$(compose logs --no-color --tail "$LOG_LINES" 2>&1 || true)"
|
||||
|
||||
if [[ -s "$MASK_FILE" ]]; then
|
||||
while IFS= read -r encoded; do
|
||||
[[ -n "$encoded" ]] || continue
|
||||
secret="$(printf '%s' "$encoded" | base64 -d)"
|
||||
[[ -n "$secret" ]] && logs="${logs//"$secret"/***}"
|
||||
done < "$MASK_FILE"
|
||||
fi
|
||||
printf '%s\n' "$logs"
|
||||
}
|
||||
|
||||
remove_by_label() {
|
||||
local kind="$1" filter="label=com.docker.compose.project=${PROJECT}"
|
||||
case "$kind" in
|
||||
container) docker ps -aq --filter "$filter" | xargs -r docker rm -f >/dev/null ;;
|
||||
network) docker network ls -q --filter "$filter" | xargs -r docker network rm >/dev/null ;;
|
||||
volume) docker volume ls -q --filter "$filter" | xargs -r docker volume rm >/dev/null ;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_cleanup() {
|
||||
local markers foreign status=0
|
||||
PROJECT="$(project_name)"
|
||||
|
||||
markers="$(docker ps -a \
|
||||
--filter "label=com.docker.compose.project=${PROJECT}" \
|
||||
--format "[{{.Label \"${CI_MARKER_LABEL}\"}}]")" \
|
||||
|| die "impossible de lister les conteneurs du projet ${PROJECT}"
|
||||
foreign="$(grep -cvxF '[true]' <<<"$markers" || true)"
|
||||
[[ -n "$markers" ]] || foreign=0
|
||||
if (( foreign > 0 )); then
|
||||
die "${foreign} conteneur(s) du projet ${PROJECT} sans marqueur de CI : nettoyage annulé"
|
||||
fi
|
||||
|
||||
if [[ -f "$CI_FILE" ]]; then
|
||||
compose down --volumes --remove-orphans --rmi local --timeout 30 || status=1
|
||||
fi
|
||||
remove_by_label container || status=1
|
||||
remove_by_label network || status=1
|
||||
remove_by_label volume || status=1
|
||||
|
||||
# shellcheck disable=SC2016 # $1 est développé par le sh du conteneur
|
||||
helper -v "${HOST_ROOT}:/ci-root" "$HELPER_IMAGE" \
|
||||
sh -c 'rm -rf -- "/ci-root/$1"' _ "$PROJECT" || status=1
|
||||
|
||||
if (( status )); then
|
||||
die "nettoyage incomplet du projet ${PROJECT}"
|
||||
fi
|
||||
echo "Projet ${PROJECT} nettoyé"
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ -n "${GITHUB_WORKSPACE:-}" ]] || die "GITHUB_WORKSPACE absent"
|
||||
case "${1:-}" in
|
||||
prepare) cmd_prepare ;;
|
||||
up) cmd_up ;;
|
||||
verify) cmd_verify ;;
|
||||
logs) cmd_logs ;;
|
||||
cleanup) cmd_cleanup ;;
|
||||
*) die "usage : $0 prepare|up|verify|logs|cleanup" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,65 +0,0 @@
|
||||
# Template
|
||||
name: Deployment Verification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
uses: docker/[email protected]
|
||||
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/[email protected]
|
||||
|
||||
- name: Create necessary Docker networks
|
||||
run: |
|
||||
docker network create back_network || true
|
||||
docker network create traefik_front_network || true
|
||||
|
||||
- name: Start up services using Docker Compose
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
# - name: Modify /etc/hosts for internal routing
|
||||
# run: |
|
||||
# echo "127.0.0.1 traefik.tips-of-mine.com" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Print Docker Compose services status
|
||||
run: docker ps
|
||||
|
||||
- name: Wait for the application to be ready via Traefik
|
||||
run: |
|
||||
echo "Checking the routing and availability of application via Traefik..."
|
||||
timeout 5m bash -c 'while ! curl -fsSLk "https://traefik.tips-of-mine.com"; do echo "Waiting for the application to be ready..."; sleep 10; done'
|
||||
|
||||
- name: Inspect Network Configuration
|
||||
run: |
|
||||
docker network inspect back_network
|
||||
docker network inspect traefik_front_network
|
||||
|
||||
- name: Shutdown Docker Compose services
|
||||
if: always()
|
||||
run: docker compose -f docker-compose.yml down
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
docker compose --profile setup down
|
||||
rm -rf /workspace/tips-of-mine/traefik/*
|
||||
@@ -1,51 +0,0 @@
|
||||
# Template
|
||||
name: Deployment Verification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create necessary Docker networks
|
||||
run: |
|
||||
docker network create back_network_{ vars.APPLICATION_NAME } || true
|
||||
docker network create traefik_front_network || true
|
||||
|
||||
- name: Start up services using Docker Compose
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
# - name: Modify /etc/hosts for internal routing
|
||||
# run: |
|
||||
# echo "127.0.0.1 { vars.APPLICATION_URL }" | sudo tee -a /etc/hosts
|
||||
# echo "127.0.0.1 dashboard.tips-of-mine.com" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Print Docker Compose services status
|
||||
run: docker ps
|
||||
|
||||
- name: Wait for the application to be ready via Traefik
|
||||
run: |
|
||||
echo "Checking the routing and availability of application via Traefik..."
|
||||
timeout 5m bash -c 'while ! curl -fsSLk "https://{ vars.APPLICATION_URL }"; do echo "Waiting for the application to be ready..."; sleep 10; done'
|
||||
|
||||
- name: Inspect Network Configuration
|
||||
run: |
|
||||
docker network inspect back_network_{ vars.APPLICATION_NAME }
|
||||
docker network inspect traefik_front_network
|
||||
|
||||
- name: Shutdown Docker Compose services
|
||||
if: always()
|
||||
run: docker compose -f docker-compose.yml down
|
||||
@@ -0,0 +1,63 @@
|
||||
# Template — vérification isolée d'une stack Docker Compose.
|
||||
# À copier tel quel dans chaque dépôt, avec .gitea/scripts/ ; aucune variable à configurer.
|
||||
# Remplace ci-develop.yml et ci-main.yml : les supprimer du dépôt.
|
||||
name: Compose CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI_SCRIPT: .gitea/scripts/compose-ci.sh
|
||||
HELPER_IMAGE: python:3.13-alpine3.22
|
||||
SHELLCHECK_IMAGE: koalaman/shellcheck:v0.10.0
|
||||
|
||||
jobs:
|
||||
lint-ci-scripts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: ShellCheck du script de CI
|
||||
run: docker run --rm -i --network none "$SHELLCHECK_IMAGE" - < "$CI_SCRIPT"
|
||||
|
||||
- name: Syntaxe du script de réécriture
|
||||
run: >-
|
||||
docker run --rm -i --network none "$HELPER_IMAGE"
|
||||
python3 -c "import ast, sys; ast.parse(sys.stdin.read())"
|
||||
< .gitea/scripts/compose-ci-rewrite.py
|
||||
|
||||
compose-verify:
|
||||
needs: lint-ci-scripts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Préparer une configuration isolée
|
||||
run: bash "$CI_SCRIPT" prepare
|
||||
|
||||
- name: Démarrer la stack de CI
|
||||
run: bash "$CI_SCRIPT" up
|
||||
|
||||
- name: Vérifier l'état des conteneurs
|
||||
run: bash "$CI_SCRIPT" verify
|
||||
|
||||
- name: Journaux (en cas d'échec)
|
||||
if: failure()
|
||||
run: bash "$CI_SCRIPT" logs
|
||||
|
||||
- name: Nettoyer la stack de CI
|
||||
if: always()
|
||||
run: bash "$CI_SCRIPT" cleanup
|
||||
@@ -12,3 +12,7 @@
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Variables d'environnement et secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
Whitespace-only changes.
@@ -7,8 +7,7 @@ http:
|
||||
service: api@internal
|
||||
middlewares:
|
||||
- dashboardauth
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
tls: {}
|
||||
traefik-http-redirect:
|
||||
rule: Host(`dashboard.tips-of-mine.com`)
|
||||
entryPoints:
|
||||
@@ -20,4 +19,4 @@ http:
|
||||
dashboardauth:
|
||||
basicAuth:
|
||||
users:
|
||||
- "admin:$2y$10$GXOzS6L1s3gwQb8zO90LKOGuvZfurIXCBgJjZ5ib9/p5l3cy1sid6"
|
||||
- "admin:$2y$10$GXOzS6L1s3gwQb8zO90LKOGuvZfurIXCBgJjZ5ib9/p5l3cy1sid6"
|
||||
@@ -1,27 +1,87 @@
|
||||
http:
|
||||
middlewares:
|
||||
cloudflarewarp:
|
||||
plugin:
|
||||
cloudflarewarp:
|
||||
disableDefault: false
|
||||
trustip:
|
||||
- "172.18.0.1/32"
|
||||
my-crowdsec-bouncer-traefik-plugin:
|
||||
plugin:
|
||||
crowdsec-bouncer-traefik-plugin:
|
||||
enabled: "true"
|
||||
crowdsecMode: stream
|
||||
crowdsecLapiScheme: http
|
||||
crowdsecLapiHost: crowdsec-app:8080
|
||||
crowdsecLapiKeyFile: /etc/traefik/lapi-key
|
||||
forwardedHeadersTrustedIPs:
|
||||
- 172.18.0.0/16
|
||||
forwardedHeadersCustomName: CF-Connecting-IP
|
||||
clientTrustedIPs:
|
||||
- 10.0.4.0/24
|
||||
- 82.66.77.254/32
|
||||
# CrowdsecLapiKey: 4yn3ayFS4V6fvlXFEvVHj2qRUE57Wuww5pT48BD2
|
||||
# Enabled: "true"
|
||||
my-fail2ban:
|
||||
plugin:
|
||||
fail2ban:
|
||||
allowlist:
|
||||
ip: ::1,127.0.0.1,10.0.4.0/24
|
||||
denylist:
|
||||
ip: 192.168.0.0/24
|
||||
rules:
|
||||
bantime: 3h
|
||||
enabled: "true"
|
||||
findtime: 10m
|
||||
maxretry: "4"
|
||||
statuscode: 400,401,403-499
|
||||
my-traefik-maintenance:
|
||||
plugin:
|
||||
traefik-maintenance:
|
||||
enabled: "true"
|
||||
filename: ./maintenance_test.html
|
||||
httpContentType: text/html; charset=utf-8
|
||||
httpResponseCode: "503"
|
||||
triggerFilename: ./maintenance_test.trigger
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
hsts-minimal:
|
||||
# hsts-minimal:
|
||||
# headers:
|
||||
# stsSeconds: 31536000
|
||||
# stsIncludeSubdomains: false
|
||||
# stsPreload: false
|
||||
# forceSTSHeader: true
|
||||
# hsts-standard:
|
||||
# headers:
|
||||
# stsSeconds: 31536000
|
||||
# stsIncludeSubdomains: true
|
||||
# stsPreload: false
|
||||
# forceSTSHeader: true
|
||||
##########################################
|
||||
hsts-headers:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: false
|
||||
stsPreload: false
|
||||
forceSTSHeader: true
|
||||
hsts-standard:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: false
|
||||
forceSTSHeader: true
|
||||
hsts-full:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
frameDeny: true
|
||||
sslRedirect: true
|
||||
browserXssFilter: true
|
||||
contentTypeNosniff: true
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
forceSTSHeader: true
|
||||
stsSeconds: 31536000
|
||||
forceStsHeader: true
|
||||
referrerPolicy: same-origin
|
||||
customResponseHeaders:
|
||||
permissions-Policy: vibrate=(self), geolocation=(self), midi=(self), notifications=(self), push=(self), microphone=(), $
|
||||
X-Permitted-Cross-Domain-Policies: none
|
||||
expect-ct: max-age=604800, report-uri="https://oak.ct.letsencrypt.org/2021"
|
||||
##########################################
|
||||
# hsts-full:
|
||||
# headers:
|
||||
# stsSeconds: 31536000
|
||||
# stsIncludeSubdomains: true
|
||||
# stsPreload: true
|
||||
# forceSTSHeader: true
|
||||
|
||||
# Redirect non-www URLs to their www equivalent
|
||||
# Use with traefik.http.routers.myRouter.middlewares: "redirect-non-www-to-www@file"
|
||||
@@ -82,9 +142,9 @@ http:
|
||||
# crowdsecLapiScheme: http
|
||||
# forwardedHeadersTrustedIPs:
|
||||
# - 10.0.35.4/32 # Cloudflare tunnel IP address
|
||||
# - 172.30.0.0/24 # Reverse Proxy IP address
|
||||
# - 172.30.0.0/24 # Reverse Proxy IP address
|
||||
# clientTrustedIPs:
|
||||
# - 10.0.1.0/24 # Internal LAN IP addresses
|
||||
# - 10.0.1.0/24 # Internal LAN IP addresses
|
||||
# - 10.0.2.0/24 # Internal LAN IP addresses
|
||||
# - 10.0.3.0/24 # Internal LAN IP addresses
|
||||
# - 10.0.4.0/24 # Internal LAN IP addresses
|
||||
@@ -102,4 +162,4 @@ http:
|
||||
# chain:
|
||||
# middlewares:
|
||||
# - default-whitelist
|
||||
# - default-headers
|
||||
# - default-headers
|
||||
@@ -1,13 +0,0 @@
|
||||
http:
|
||||
routers:
|
||||
harbor:
|
||||
service: harbor
|
||||
rule: "Host(`registry.tips-of-mine.com`)"
|
||||
tls:
|
||||
certResolver: production
|
||||
|
||||
services:
|
||||
harbor:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://172.27.0.1:8880"
|
||||
@@ -1,13 +1,43 @@
|
||||
http:
|
||||
routers:
|
||||
harbor:
|
||||
service: harbor
|
||||
rule: "Host(`registry.tips-of-mine.com`)"
|
||||
tls:
|
||||
certResolver: production
|
||||
|
||||
# entrypoints: "http"
|
||||
# harbor-https:
|
||||
# rule: "Host(`registry.tips-of-mine.com`)"
|
||||
# entrypoints: "https"
|
||||
service: harbor
|
||||
# middlewares: harbor
|
||||
tls: {}
|
||||
# certResolver: letsencrypt
|
||||
# observability:
|
||||
# accesslogs: true
|
||||
# metrics: true
|
||||
# tracing: true
|
||||
services:
|
||||
harbor:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://10.0.4.133:8083"
|
||||
# responseForwarding:
|
||||
# flushInterval: 100ms
|
||||
# responseforwardingtimeouts:
|
||||
# readtimeout: "900s"
|
||||
# writetimeout: "900s"
|
||||
# middlewares:
|
||||
# harbor:
|
||||
# headers:
|
||||
# customrequestheaders:
|
||||
# X-Forwarded-Proto: "https"
|
||||
# X-Forwarded-Port: "443"
|
||||
# X-Real-IP: ""
|
||||
# buffering:
|
||||
# maxRequestBodyBytes: 0
|
||||
# memRequestBodyBytes: 0
|
||||
# limit:
|
||||
# buffering:
|
||||
# maxRequestBodyBytes: 5368709120
|
||||
# memRequestBodyBytes: 10485760
|
||||
# buffering:
|
||||
# maxRequestBodyBytes: 5368709120
|
||||
# memRequestBodyBytes: 10485760
|
||||
+26
-2
@@ -2,13 +2,37 @@ tls:
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /etc/traefik/ssl/cert.pem
|
||||
keyFile: /etc/traefik/ssl/privkey.pem
|
||||
certFile: /etc/traefik/ssl/wildcard.tips-of-mine.com.crt
|
||||
keyFile: /etc/traefik/ssl/wildcard.tips-of-mine.com.key
|
||||
# certFile: /etc/traefik/ssl/cert.pem
|
||||
# keyFile: /etc/traefik/ssl/privkey.pem
|
||||
certificates:
|
||||
- certFile: /etc/traefik/ssl/cert.pem
|
||||
keyFile: /etc/traefik/ssl/privkey.pem
|
||||
- certFile: /etc/traefik/ssl/wildcard.tips-of-mine.com.crt
|
||||
keyFile: /etc/traefik/ssl/wildcard.tips-of-mine.com.key
|
||||
##############
|
||||
stores:
|
||||
- default
|
||||
##############
|
||||
options:
|
||||
default:
|
||||
minVersion: VersionTLS12
|
||||
###############
|
||||
# sniStrict: true
|
||||
cipherSuites:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 # TLS 1.2
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 # TLS 1.2
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 # TLS 1.2
|
||||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 # TLS 1.2
|
||||
- TLS_AES_256_GCM_SHA384 # TLS 1.3
|
||||
- TLS_CHACHA20_POLY1305_SHA256 # TLS 1.3
|
||||
- TLS_FALLBACK_SCSV # TLS FALLBACK
|
||||
curvePreferences:
|
||||
- secp521r1
|
||||
- secp384r1
|
||||
modern:
|
||||
minVersion: VersionTLS13
|
||||
#######
|
||||
mintls13:
|
||||
minVersion: VersionTLS1
|
||||
+92
-49
@@ -3,31 +3,34 @@
|
||||
global:
|
||||
# Send anonymous usage data
|
||||
sendAnonymousUsage: false
|
||||
checkNewVersion: true
|
||||
checkNewVersion: false
|
||||
|
||||
entryPoints:
|
||||
# ftp:
|
||||
# address: ":21"
|
||||
ssh:
|
||||
address: ":22"
|
||||
# ssh:
|
||||
# address: ":22"
|
||||
# smtp:
|
||||
# address: ":25"
|
||||
# dns:
|
||||
# address: ":53"
|
||||
http:
|
||||
address: ":80"
|
||||
forwardedHeaders:
|
||||
insecure: true
|
||||
trustedIPs: &trustedIps
|
||||
- 10.0.4.0/24
|
||||
# transport:
|
||||
# respondingTimeouts:
|
||||
# idleTimeout: 600
|
||||
# writeTimeout: 600
|
||||
# readTimeout: 600
|
||||
## forwardedHeaders:
|
||||
## insecure: true
|
||||
## trustedIPs: &trustedIps
|
||||
## - 10.0.4.0/24
|
||||
http:
|
||||
# middlewares: # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
# - "crowdsec@file" # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
# - "cloudflarewarp@file" # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: https
|
||||
scheme: https
|
||||
permanent: true
|
||||
# ssh:
|
||||
# address: ":22"
|
||||
# pop3:
|
||||
@@ -36,16 +39,29 @@ entryPoints:
|
||||
# address: ":143"
|
||||
https:
|
||||
address: ":443"
|
||||
forwardedHeaders:
|
||||
insecure: true
|
||||
trustedIPs: &trustedIps
|
||||
# http:
|
||||
# middlewares:
|
||||
# transport:
|
||||
# respondingTimeouts:
|
||||
# idleTimeout: 600
|
||||
# writeTimeout: 600
|
||||
# readTimeout: 600
|
||||
## forwardedHeaders:
|
||||
## insecure: true
|
||||
## trustedIPs: &trustedIps
|
||||
http:
|
||||
middlewares:
|
||||
- cloudflarewarp@file
|
||||
- my-crowdsec-bouncer-traefik-plugin@file
|
||||
# - error-pages@file
|
||||
# - sablier@file
|
||||
# - crowdsec-bouncer@file
|
||||
# - cloudflarewarp@file" # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
# - secureHeaders@file
|
||||
# tls:
|
||||
tls: {}
|
||||
# certResolver: letsencrypt
|
||||
# domains:
|
||||
# - main: "tips-of-mine.com"
|
||||
# sans:
|
||||
# - "*.tips-of-mine.com"
|
||||
# smtp-ssl:
|
||||
# address: ":465"
|
||||
# starttls:
|
||||
@@ -58,10 +74,21 @@ entryPoints:
|
||||
# address: ":1194/udp"
|
||||
# mysql:
|
||||
# address: ":3306"
|
||||
# lcdlln:
|
||||
# address: "3841"
|
||||
vault:
|
||||
address: ":8200"
|
||||
forwardedHeaders:
|
||||
trustedIPs:
|
||||
- 0.0.0.0/0
|
||||
# elasticsearch:
|
||||
# address: ":9200"
|
||||
metrics:
|
||||
address: ":8181"
|
||||
tcptest:
|
||||
address: ":9090"
|
||||
udptest:
|
||||
address: ":9091/udp"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
@@ -75,41 +102,65 @@ providers:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
providersThrottleDuration: 10
|
||||
swarmMode: false
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: [email protected]
|
||||
storage: /var/traefik/certs/acme.json
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
keyType: EC256
|
||||
dnsChallenge:
|
||||
provider: letsencrypt
|
||||
resolvers:
|
||||
- "8.8.8.8:53"
|
||||
- "1.1.1.1:53"
|
||||
- "1.0.0.1:53"
|
||||
delaybeforecheck: 300
|
||||
tlschallenge: true
|
||||
# httpchallenge:
|
||||
# entrypoint: http
|
||||
#certificatesResolvers:
|
||||
# letsencrypt:
|
||||
# acme:
|
||||
# email: [email protected]
|
||||
# storage: /etc/traefik/acme/letsencrypt.json
|
||||
# caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
# keyType: EC256
|
||||
# dnsChallenge:
|
||||
# provider: cloudflare
|
||||
# resolvers:
|
||||
# - "1.1.1.1:53"
|
||||
# - "8.8.8.8:53"
|
||||
# delayBeforeCheck: 0
|
||||
## caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
## keyType: EC256
|
||||
## dnsChallenge:
|
||||
## provider: cloudflare
|
||||
## resolvers:
|
||||
## - "8.8.8.8:53"
|
||||
## delaybeforecheck: 300
|
||||
# tlschallenge: true
|
||||
## httpchallenge:
|
||||
## entrypoint: http
|
||||
|
||||
api:
|
||||
insecure: true
|
||||
# 13/05/2025 : true
|
||||
insecure: false
|
||||
dashboard: true
|
||||
|
||||
log:
|
||||
level: ${LOG_LEVEL:-INFO}"
|
||||
level: INFO
|
||||
filepath: "/var/log/traefik/traefik.log"
|
||||
format: json
|
||||
# default: "common"
|
||||
|
||||
accesslog:
|
||||
# level: INFO
|
||||
filepath: "/var/log/traefik/access.log"
|
||||
format: json
|
||||
bufferingSize: 100
|
||||
# format: common
|
||||
filters:
|
||||
statusCodes: ["200-299", "400-499", "500-599"]
|
||||
fields:
|
||||
defaultMode: keep
|
||||
names:
|
||||
RequestHost: keep
|
||||
headers:
|
||||
defaultMode: keep
|
||||
names:
|
||||
Host: keep
|
||||
|
||||
# Tracing
|
||||
tracing:
|
||||
serviceName: traefik
|
||||
sampleRate: 1.0
|
||||
otlp:
|
||||
http:
|
||||
endpoint: http://tempo-app:4318/v1/traces
|
||||
|
||||
# Ajout de la partie métrique qui concerne Prometheus
|
||||
metrics:
|
||||
@@ -132,16 +183,8 @@ experimental:
|
||||
plugins:
|
||||
crowdsec-bouncer-traefik-plugin:
|
||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
version: "v1.4.5"
|
||||
|
||||
cloudflarewarp:
|
||||
moduleName: "github.com/BetterCorp/cloudflarewarp"
|
||||
version: "v1.3.3"
|
||||
|
||||
traefik-maintenance:
|
||||
moduleName: "github.com/TRIMM/traefik-maintenance"
|
||||
version: "v1.0.1"
|
||||
|
||||
fail2ban:
|
||||
moduleName: "github.com/tomMoulard/fail2ban"
|
||||
version: "v0.8.3"
|
||||
|
||||
sablier:
|
||||
moduleName: "github.com/acouvreur/sablier"
|
||||
version: "v1.8.0-beta.22"
|
||||
@@ -1,158 +0,0 @@
|
||||
## static configuration
|
||||
|
||||
global:
|
||||
# Send anonymous usage data
|
||||
sendAnonymousUsage: false
|
||||
checkNewVersion: true
|
||||
|
||||
entryPoints:
|
||||
# ftp:
|
||||
# address: ":21"
|
||||
ssh:
|
||||
address: ":22"
|
||||
# smtp:
|
||||
# address: ":25"
|
||||
# dns:
|
||||
# address: ":53"
|
||||
http:
|
||||
address: ":80"
|
||||
forwardedHeaders:
|
||||
insecure: true
|
||||
trustedIPs: &trustedIps
|
||||
- 10.0.4.0/24
|
||||
http:
|
||||
# middlewares: # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
# - "crowdsec@file" # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
# - "cloudflarewarp@file" # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: https
|
||||
scheme: https
|
||||
# gitea-ssh:
|
||||
# address: ":2222"
|
||||
# pop3:
|
||||
# address: ":110"
|
||||
# imap:
|
||||
# address: ":143"
|
||||
https:
|
||||
address: ":443"
|
||||
forwardedHeaders:
|
||||
insecure: true
|
||||
trustedIPs: &trustedIps
|
||||
# http:
|
||||
# middlewares:
|
||||
# - crowdsec-bouncer@file
|
||||
# - cloudflarewarp@file" # CHANGE MADE HERE (BOUNCER ENABLED) !!!
|
||||
# - secureHeaders@file
|
||||
# tls:
|
||||
# certResolver: letsencrypt
|
||||
# smtp-ssl:
|
||||
# address: ":465"
|
||||
# starttls:
|
||||
# address: ":587"
|
||||
# imap-ssl:
|
||||
# address: ":993"
|
||||
# pop3-ssl:
|
||||
# address: ":995"
|
||||
# openvpn:
|
||||
# address: ":1194/udp"
|
||||
# mysql:
|
||||
# address: ":3306"
|
||||
# elasticsearch:
|
||||
# address: ":9200"
|
||||
metrics:
|
||||
address: ":8181"
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
providers:
|
||||
docker:
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
exposedByDefault: false
|
||||
watch: true
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
providersThrottleDuration: 10
|
||||
|
||||
certificatesResolvers:
|
||||
staging:
|
||||
acme:
|
||||
email: [email protected]
|
||||
storage: acme.json
|
||||
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
keyType: EC256
|
||||
dnsChallenge:
|
||||
provider: staging
|
||||
resolvers:
|
||||
- "1.1.1.1:53"
|
||||
- "1.0.0.1:53"
|
||||
tlschallenge: true
|
||||
# httpChallenge:
|
||||
# entryPoint: http
|
||||
production:
|
||||
acme:
|
||||
email: [email protected]
|
||||
storage: acme.json
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
keyType: EC256
|
||||
dnsChallenge:
|
||||
provider: production
|
||||
resolvers:
|
||||
- "1.1.1.1:53"
|
||||
- "1.0.0.1:53"
|
||||
tlschallenge: true
|
||||
# httpchallenge:
|
||||
# entrypoint: http
|
||||
|
||||
api:
|
||||
insecure: true
|
||||
dashboard: true
|
||||
|
||||
log:
|
||||
level: DEBUG
|
||||
filepath: "/var/log/traefik/traefik.log"
|
||||
format: json
|
||||
# default: "common"
|
||||
|
||||
accesslog:
|
||||
filepath: "/var/log/traefik/access.log"
|
||||
format: json
|
||||
bufferingSize: 100
|
||||
# format: common
|
||||
|
||||
# Ajout de la partie métrique qui concerne Prometheus
|
||||
metrics:
|
||||
prometheus:
|
||||
# Nom du point d'entrée défini au dessus
|
||||
entryPoint: metrics
|
||||
# On configure la latence des métriques
|
||||
buckets:
|
||||
- 0.1
|
||||
- 0.3
|
||||
- 1.2
|
||||
- 5.0
|
||||
# Ajout des métriques sur les points d'entrée
|
||||
addEntryPointsLabels: true
|
||||
# Ajout des services
|
||||
addServicesLabels: true
|
||||
addRoutersLabels: true
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
crowdsec-bouncer-traefik-plugin:
|
||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
version: "v1.3.3"
|
||||
|
||||
traefik-maintenance:
|
||||
moduleName: "github.com/TRIMM/traefik-maintenance"
|
||||
version: "v1.0.1"
|
||||
|
||||
fail2ban:
|
||||
moduleName: "github.com/tomMoulard/fail2ban"
|
||||
version: "v0.8.3"
|
||||
|
||||
sablier:
|
||||
moduleName: "github.com/acouvreur/sablier"
|
||||
version: "v1.8.0-beta.22"
|
||||
+87
-21
@@ -1,4 +1,3 @@
|
||||
### networks
|
||||
networks:
|
||||
back_network:
|
||||
driver: bridge
|
||||
@@ -24,6 +23,14 @@ services:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8181:8181"
|
||||
- "9090:9090"
|
||||
- "9091:9091/udp"
|
||||
# - "3841:3841"
|
||||
# - "25:25" # SMTP (explicit TLS => STARTTLS, Authentication is DISABLED => use port 465/587 instead)
|
||||
# - "143:143" # IMAP4 (explicit TLS => STARTTLS)
|
||||
# - "465:465" # ESMTP (implicit TLS)
|
||||
# - "587:587" # ESMTP (explicit TLS => STARTTLS)
|
||||
# - "993:993" # IMAP4 (implicit TLS)
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "/etc/localtime:/etc/localtime:ro"
|
||||
@@ -32,34 +39,23 @@ services:
|
||||
# - "./certificates/acme.json:/etc/traefik/acme/acme.json:rw"
|
||||
- "./certificates:/var/traefik/certs:rw"
|
||||
- "./certificates:/etc/traefik/ssl:rw"
|
||||
- "./certificates:/certs:ro"
|
||||
- "./logs:/var/log/traefik:rw"
|
||||
- "./acme:/etc/traefik/acme:rw"
|
||||
- "./crowdsec/lapi-key:/etc/traefik/lapi-key:ro"
|
||||
environment:
|
||||
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
|
||||
- CLOUDFLARE_DNS_API_TOKEN_FILE=/run/secrets/cf_token
|
||||
# - CLOUDFLARE_DNS_API_TOKEN_FILE=/run/secrets/cf_token
|
||||
- CLOUDFLARE_HTTP_TIMEOUT=${HTTP_TIMEOUT}
|
||||
- CLOUDFLARE_POLLING_INTERVAL=${POLLING_INTERVAL}
|
||||
- CLOUDFLARE_PROPAGATION_TIMEOUT=${PROPAGATION_TIMEOUT}
|
||||
- CLOUDFLARE_TTL=${TTL}
|
||||
# Swarm config
|
||||
# deploy:
|
||||
# restart_policy:
|
||||
# condition: any
|
||||
# delay: 5s
|
||||
# max_attempts: 3
|
||||
# window: 120s
|
||||
# update_config:
|
||||
# delay: 10s
|
||||
# order: start-first
|
||||
# parallelism: 1
|
||||
# rollback_config:
|
||||
# parallelism: 0
|
||||
# order: stop-first
|
||||
# placement:
|
||||
# constraints:
|
||||
# - node.role == manager
|
||||
networks:
|
||||
- back_network
|
||||
- front_network
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "fathom.vault.CF_DNS_API_TOKEN=secret/cloudflare"
|
||||
|
||||
### Certificats
|
||||
certificat:
|
||||
@@ -73,18 +69,52 @@ services:
|
||||
- "./certificates:/etc/traefik/ssl"
|
||||
networks:
|
||||
- front_network
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
### Crowdsec:
|
||||
crowdsec:
|
||||
container_name: crowdsec-app
|
||||
hostname: crowdsec-app
|
||||
image: crowdsecurity/crowdsec:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8090:8080"
|
||||
environment:
|
||||
COLLECTIONS: "crowdsecurity/linux crowdsecurity/traefik"
|
||||
GID: "1000"
|
||||
# depends_on:
|
||||
# - "reverse-proxy"
|
||||
volumes:
|
||||
- "./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml"
|
||||
- "./logs:/var/log/traefik/:ro"
|
||||
- "./crowdsec-db:/var/lib/crowdsec/data/"
|
||||
- "./crowdsec-config:/etc/crowdsec/"
|
||||
networks:
|
||||
- front_network
|
||||
|
||||
### error-pages
|
||||
error-pages:
|
||||
image: ghcr.io/tarampampam/error-pages:3
|
||||
container_name: traefik-error-pages
|
||||
environment:
|
||||
- TEMPLATE_NAME=l7
|
||||
networks:
|
||||
- front_network
|
||||
restart: unless-stopped
|
||||
|
||||
### whoami
|
||||
whoami:
|
||||
container_name: traefik-whoami
|
||||
hostname: traefik-whoami
|
||||
image: traefik/whoami:latest
|
||||
command:
|
||||
- --name=externe
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --name=externe
|
||||
networks:
|
||||
- front_network
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=front_network"
|
||||
# HTTP
|
||||
@@ -96,5 +126,41 @@ services:
|
||||
- "traefik.http.routers.whoami-https.tls=true"
|
||||
- "traefik.http.routers.whoami-https.service=whoami-service"
|
||||
# Middleware
|
||||
# - "traefik.http.routers.whoami-https.middlewares=forward-auth"
|
||||
# - "traefik.http.routers.whoami-https.middlewares=cloudflarewarp@file,my-crowdsec-bouncer-traefik-plugin@file"
|
||||
# Service
|
||||
- "traefik.http.services.whoami-service.loadbalancer.server.port=80"
|
||||
# Homepage
|
||||
- "homepage.group=Tools"
|
||||
- "homepage.name=whoami"
|
||||
- "homepage.icon=whoami.png"
|
||||
- "homepage.href=https://whoami.tips-of-mine.com"
|
||||
- "homepage.description=whoami"
|
||||
|
||||
whoami-tcp:
|
||||
image: traefik/whoamitcp:latest
|
||||
container_name: traefik-whoami-tcp
|
||||
command:
|
||||
- "-name=traefik-whoami-tcp"
|
||||
networks:
|
||||
- front_network
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# - "traefik.docker.network=front_network"
|
||||
- "traefik.tcp.routers.whoami-tcp.entrypoints=tcptest"
|
||||
- "traefik.tcp.routers.whoami-tcp.rule=HostSNI(`*`)"
|
||||
- "traefik.tcp.services.whoami-tcp.loadbalancer.server.port=8080"
|
||||
|
||||
whoami-udp:
|
||||
image: traefik/whoamiudp:latest
|
||||
container_name: traefik-whoami-udp
|
||||
command:
|
||||
- "-name=traefik-whoami-udp"
|
||||
networks:
|
||||
- front_network
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.udp.routers.whoami-udp.entrypoints=udptest"
|
||||
- "traefik.udp.services.whoami-udp.loadbalancer.server.port=8080"
|
||||
Reference in new issue
Block a user