Revue statique, conventions documentees, construction arm64 limitee aux versions et cache de couches
build / Garde-fou (pull_request) Successful in 9m21s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (pull_request) Skipped
build / Images Harbor (web, Dockerfile) (pull_request) Skipped
build / Garde-fou (push) Successful in 9m20s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (push) Successful in 9m44s
build / Images Harbor (web, Dockerfile) (push) Successful in 13m7s
build / Garde-fou (pull_request) Successful in 9m21s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (pull_request) Skipped
build / Images Harbor (web, Dockerfile) (pull_request) Skipped
build / Garde-fou (push) Successful in 9m20s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (push) Successful in 9m44s
build / Images Harbor (web, Dockerfile) (push) Successful in 13m7s
This commit is contained in:
1 parent
6d02ec0c18
commit
acf72da7f8
16 files changed
+193
-46
No files matched your search
@@ -27,7 +27,10 @@ jobs:
|
||||
# evalue mal cette expression dans un bloc "with". La variable
|
||||
# ci-dessus reste la reference lue par les humains.
|
||||
python-version: "3.12"
|
||||
- run: pip install -r requirements.txt pytest
|
||||
cache: pip
|
||||
- run: pip install -r requirements.txt pytest ruff
|
||||
- name: Revue statique
|
||||
run: python -m ruff check .
|
||||
- run: python -m pytest -q
|
||||
- name: Coherence du catalogue
|
||||
run: python -m enclume.cli --list > /dev/null
|
||||
@@ -48,7 +51,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# QEMU n'est necessaire que pour l'arm64, donc uniquement sur les tags.
|
||||
- name: Preparer QEMU
|
||||
if: gitea.ref_type == 'tag'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Preparer Buildx
|
||||
@@ -72,13 +77,24 @@ jobs:
|
||||
type=sha,format=long
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Choisir les architectures
|
||||
id: arch
|
||||
run: |
|
||||
# L'arm64 passe par l'emulation QEMU et coute plusieurs dizaines de
|
||||
# minutes. On ne le paie qu'au moment de publier une version.
|
||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||
echo "plateformes=linux/amd64,linux/arm64" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "plateformes=linux/amd64" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Construire et pousser
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ steps.arch.outputs.plateformes }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -90,6 +106,10 @@ jobs:
|
||||
# unknown/unknown que Harbor affiche mal et que son scanner ignore.
|
||||
provenance: false
|
||||
sbom: false
|
||||
# Sans cache, chaque construction reinstalle toutes les dependances.
|
||||
# Le cache vit dans Harbor, a cote des images.
|
||||
cache-from: type=registry,ref=${{ vars.HARBOR_HOST }}/${{ vars.HARBOR_PROJECT }}/${{ matrix.composant }}:cache
|
||||
cache-to: type=registry,ref=${{ vars.HARBOR_HOST }}/${{ vars.HARBOR_PROJECT }}/${{ matrix.composant }}:cache,mode=max
|
||||
|
||||
- name: Declencher le scan Harbor
|
||||
env:
|
||||
|
||||
+5
-2
@@ -6,9 +6,12 @@ FROM python:3.12-slim AS build
|
||||
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
WORKDIR /build
|
||||
|
||||
# requirements.txt seul dans cette couche : tant qu'il ne change pas, la
|
||||
# reinstallation des dependances est reprise du cache, meme si le code a bouge.
|
||||
COPY requirements.txt ./
|
||||
RUN python -m venv /opt/venv \
|
||||
&& /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
python -m venv /opt/venv \
|
||||
&& /opt/venv/bin/pip install -r requirements.txt
|
||||
|
||||
# ------------------------------------------------------------------- execution
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
@@ -90,6 +90,12 @@ Un pack est un fichier YAML déposé dans `catalog/<catégorie>/`. Le format et
|
||||
commenté sont dans `catalog/exemple-pack.yml.example`. Aucun code à modifier, aucune image à
|
||||
reconstruire : `catalog-sync` le prend en charge au prochain cycle.
|
||||
|
||||
## Contribuer et livrer
|
||||
|
||||
Les conventions de nommage et la revue à passer avant chaque version sont dans
|
||||
[docs/CONVENTIONS.md](docs/CONVENTIONS.md). En résumé : `ruff check`, la suite de tests, et
|
||||
une installation en environnement vierge — dans cet ordre, avant tout tag.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Conventions et revue avant livraison
|
||||
|
||||
Ce fichier existe pour que le projet reste cohérent d'une version à l'autre, et pour qu'une
|
||||
anomalie soit trouvée sur le poste de développement plutôt qu'après soixante minutes de
|
||||
construction.
|
||||
|
||||
## Revue avant chaque livraison
|
||||
|
||||
Dans cet ordre, sans exception, y compris pour un correctif d'une ligne :
|
||||
|
||||
```bash
|
||||
python -m ruff check . # analyse statique, zéro signalement toléré
|
||||
python -m pytest -q # la suite complète
|
||||
node --check enclume/static/*.js # syntaxe des scripts
|
||||
sh -n docker/*.sh # syntaxe des scripts shell
|
||||
python -c "import yaml,glob;[yaml.safe_load(open(f)) for f in glob.glob('catalog/**/*.yml',recursive=True)]"
|
||||
docker compose -f compose.yaml config > /dev/null
|
||||
```
|
||||
|
||||
Puis, dans un environnement vierge — c'est ce qui a manqué le jour où `requests` a cassé la
|
||||
CI :
|
||||
|
||||
```bash
|
||||
python -m venv /tmp/vierge && /tmp/vierge/bin/pip install -r requirements.txt pytest
|
||||
/tmp/vierge/bin/python -m pytest -q
|
||||
```
|
||||
|
||||
Et enfin la revue humaine, qui ne s'automatise pas :
|
||||
|
||||
- toute ressource référencée par un gabarit existe-t-elle réellement ? (un test le vérifie
|
||||
désormais, parce que l'oubli a déjà eu lieu)
|
||||
- une variable nouvelle est-elle présente dans `.env.example`, dans `compose.yaml` **et**
|
||||
documentée ?
|
||||
- une valeur venant de l'utilisateur peut-elle contenir un caractère qui casse le format où
|
||||
on l'insère ? Point-virgule pour le CLAPI, `@` et `:` pour une URL de connexion, guillemet
|
||||
simple pour le shell. Trois incidents sur ce seul motif.
|
||||
- ce qui est écrit en base a-t-il traversé `strip_secrets` ?
|
||||
|
||||
## Nommage
|
||||
|
||||
| Objet | Règle | Exemple |
|
||||
|---|---|---|
|
||||
| Modules Python | français, minuscules, sans accent | `routes_donnees.py` |
|
||||
| Fonctions et variables | français, `snake_case` | `utilisateur_courant`, `_repertoire_catalogue` |
|
||||
| Classes | français, `PascalCase` | `Soumission`, `Prerequis` |
|
||||
| Variables d'environnement | préfixe `ENCLUME_`, majuscules | `ENCLUME_DB_MOTDEPASSE` |
|
||||
| Routes d'API | français, pluriel | `/api/soumissions`, `/api/partages` |
|
||||
| Identifiants HTML | français, `kebab-case` | `btn-serveur-ouvrir`, `filtre-statut` |
|
||||
| Fichiers statiques | français | `editeur.js`, `moderation.js` |
|
||||
| Tables | français, pluriel | `utilisateurs`, `partages` |
|
||||
| Packs du catalogue | `<famille>-<techno>[-<protocole>]` | `os-linux-snmp`, `hw-dell-idrac-snmp` |
|
||||
| Modèles d'hôte générés | `<Famille>-<Techno>-custom` | `OS-Linux-SNMP-custom` |
|
||||
| Modèles de service | `<Famille>-<Techno>-<Sonde>` | `OS-Linux-SNMP-Cpu` |
|
||||
| Images | `<registre>/enclume/<composant>` | `.../enclume/catalog-sync` |
|
||||
| Branches de soumission | `pack/<id>-<numéro>` | `pack/app-rabbitmq-7` |
|
||||
|
||||
Les commentaires et la documentation sont en français. Le code source reste sans accents,
|
||||
par choix, pour éviter toute question d'encodage dans les conteneurs.
|
||||
|
||||
## Versions
|
||||
|
||||
Sémantique stricte, parce que la rétention Harbor et le retour arrière s'y appuient :
|
||||
|
||||
- **correctif** (`1.1.2`) : rien de nouveau, rien qui change de nom
|
||||
- **mineure** (`1.2.0`) : fonctionnalité, nouvelle variable, migration de schéma
|
||||
- **majeure** (`2.0.0`) : rupture pour qui héberge déjà — variable supprimée, format de
|
||||
projet incompatible
|
||||
|
||||
Une migration de schéma n'ajoute que des colonnes et des tables. Une suppression attend la
|
||||
version suivante, sinon revenir à l'image précédente casse l'application.
|
||||
|
||||
## Coût de construction
|
||||
|
||||
L'arm64 passe par l'émulation QEMU et coûte l'essentiel du temps de construction. Il n'est
|
||||
donc produit **que sur un tag de version** ; un simple push sur `main` ne construit que
|
||||
l'amd64. Le cache de couches vit dans Harbor, à côté des images : tant que
|
||||
`requirements.txt` ne change pas, l'installation des dépendances est reprise du cache.
|
||||
|
||||
Conséquence pratique : itérer sur `main` est rapide, publier une version reste lent. Autant
|
||||
grouper les corrections avant de taguer.
|
||||
+4
-3
@@ -11,7 +11,8 @@ identifiants de ces services, ni a etre redeployee quand on en ajoute un.
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for
|
||||
@@ -100,7 +101,7 @@ def connexion():
|
||||
def retour():
|
||||
try:
|
||||
jeton = oauth.fournisseur.authorize_access_token()
|
||||
except Exception: # noqa: BLE001 - echec d'echange, on renvoie sans detail
|
||||
except Exception:
|
||||
current_app.logger.warning("echec de l'echange du jeton OIDC")
|
||||
return redirect("/?connexion=echec")
|
||||
|
||||
@@ -108,7 +109,7 @@ def retour():
|
||||
if not revendications:
|
||||
try:
|
||||
revendications = oauth.fournisseur.userinfo(token=jeton)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
revendications = {}
|
||||
|
||||
sujet = str(revendications.get("sub") or "").strip()
|
||||
|
||||
+5
-5
@@ -31,7 +31,7 @@ class Macro:
|
||||
is_password: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "Macro":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> Macro:
|
||||
return cls(
|
||||
name=str(raw["name"]).strip().upper(),
|
||||
value="" if raw.get("value") is None else str(raw.get("value")),
|
||||
@@ -53,7 +53,7 @@ class Service:
|
||||
max_attempts: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "Service":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> Service:
|
||||
return cls(
|
||||
name=str(raw["name"]).strip(),
|
||||
alias=str(raw.get("alias") or raw["name"]).strip(),
|
||||
@@ -77,7 +77,7 @@ class HostTemplate:
|
||||
macros: list[Macro] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "HostTemplate":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> HostTemplate:
|
||||
return cls(
|
||||
name=str(raw["name"]).strip(),
|
||||
alias=str(raw.get("alias") or raw["name"]).strip(),
|
||||
@@ -121,7 +121,7 @@ class Pack:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any], source_file: str = "") -> "Pack":
|
||||
def from_dict(cls, raw: dict[str, Any], source_file: str = "") -> Pack:
|
||||
return cls(
|
||||
id=str(raw["id"]).strip(),
|
||||
name=str(raw["name"]).strip(),
|
||||
@@ -155,7 +155,7 @@ class Catalog:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
raw = yaml.safe_load(handle)
|
||||
pack = Pack.from_dict(raw, source_file=path)
|
||||
except Exception as exc: # noqa: BLE001 - remonte tel quel a l'UI
|
||||
except Exception as exc:
|
||||
self.errors.append(f"{path} : {exc}")
|
||||
continue
|
||||
if pack.id in self.packs:
|
||||
|
||||
+12
-4
@@ -25,14 +25,22 @@ def main(argv: list[str] | None = None) -> int:
|
||||
parser.add_argument("--catalog", default=os.environ.get("ENCLUME_CATALOG", DEFAULT_CATALOG))
|
||||
parser.add_argument("--list", action="store_true", help="liste les packs disponibles")
|
||||
parser.add_argument("--pack", action="append", default=[], help="pack a importer (repetable)")
|
||||
parser.add_argument("--tout", action="store_true", help="importer toutes les sondes, pas seulement celles par defaut")
|
||||
parser.add_argument(
|
||||
"--tout",
|
||||
action="store_true",
|
||||
help="importer toutes les sondes, pas seulement celles par defaut",
|
||||
)
|
||||
parser.add_argument("--projet", help="fichier de projet JSON exporte depuis le site")
|
||||
parser.add_argument("--prefix", default="", help="prefixe applique aux objets crees")
|
||||
parser.add_argument("--plugins-dir", default="/usr/lib/centreon/plugins")
|
||||
parser.add_argument("--version-centreon", default="24.x")
|
||||
parser.add_argument("--poller", default="Central")
|
||||
parser.add_argument("--out", help="repertoire de sortie (defaut : stdout)")
|
||||
parser.add_argument("--strict", action="store_true", help="code de retour non nul si le projet contient une erreur")
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="code de retour non nul si le projet contient une erreur",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
catalog = Catalog(args.catalog)
|
||||
@@ -93,5 +101,5 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except BrokenPipeError:
|
||||
raise SystemExit(0)
|
||||
except BrokenPipeError: # sortie tronquee par un pipe, ex. | head
|
||||
raise SystemExit(0) from None
|
||||
+5
-5
@@ -11,7 +11,7 @@ tard, quand le retour arriere n'est plus envisage.
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import (
|
||||
@@ -35,7 +35,7 @@ db = SQLAlchemy(model_class=Base)
|
||||
|
||||
|
||||
def maintenant() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
def _identifiant_court() -> str:
|
||||
@@ -55,7 +55,7 @@ class Utilisateur(db.Model):
|
||||
cree_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=maintenant)
|
||||
vu_le: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=maintenant)
|
||||
|
||||
projets: Mapped[list["Projet"]] = relationship(back_populates="proprietaire", cascade="all, delete-orphan")
|
||||
projets: Mapped[list[Projet]] = relationship(back_populates="proprietaire", cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def affichage(self) -> str:
|
||||
@@ -96,7 +96,7 @@ class Partage(db.Model):
|
||||
vues: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
@classmethod
|
||||
def creer(cls, contenu: str, nom: str, jours: int, utilisateur_id: int | None = None) -> "Partage":
|
||||
def creer(cls, contenu: str, nom: str, jours: int, utilisateur_id: int | None = None) -> Partage:
|
||||
return cls(
|
||||
contenu=contenu,
|
||||
nom=nom[:255],
|
||||
@@ -108,7 +108,7 @@ class Partage(db.Model):
|
||||
def expire(self) -> bool:
|
||||
limite = self.expire_le
|
||||
if limite.tzinfo is None: # SQLite rend des datetimes naifs
|
||||
limite = limite.replace(tzinfo=timezone.utc)
|
||||
limite = limite.replace(tzinfo=UTC)
|
||||
return limite < maintenant()
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ livres a l'utilisateur ne sont que des rendus differents de cette liste.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from .model import Macro, Project, validate
|
||||
@@ -172,7 +172,7 @@ def build_plan(project: Project) -> Plan:
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _header(title: str, plan: Plan) -> list[str]:
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
stamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")
|
||||
summary = ", ".join(f"{v} {k}" for k, v in plan.summary.items() if v)
|
||||
return [
|
||||
f"# {title} - {plan.project.name}",
|
||||
|
||||
+9
-9
@@ -64,7 +64,7 @@ class Macro:
|
||||
is_password: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "Macro":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> Macro:
|
||||
return cls(
|
||||
name=_clean(raw.get("name")).upper(),
|
||||
value="" if raw.get("value") is None else str(raw.get("value")),
|
||||
@@ -81,7 +81,7 @@ class Command:
|
||||
comment: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "Command":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> Command:
|
||||
return cls(
|
||||
name=_clean(raw.get("name")),
|
||||
type=_clean(raw.get("type")) or "check",
|
||||
@@ -106,7 +106,7 @@ class ServiceTemplate:
|
||||
macros: list[Macro] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "ServiceTemplate":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> ServiceTemplate:
|
||||
def as_int(key: str, default: int) -> int:
|
||||
try:
|
||||
return int(raw.get(key, default))
|
||||
@@ -142,7 +142,7 @@ class HostTemplate:
|
||||
macros: list[Macro] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "HostTemplate":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> HostTemplate:
|
||||
return cls(
|
||||
name=_clean(raw.get("name")),
|
||||
alias=_clean(raw.get("alias")),
|
||||
@@ -167,7 +167,7 @@ class Host:
|
||||
macros: list[Macro] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "Host":
|
||||
def from_dict(cls, raw: dict[str, Any]) -> Host:
|
||||
return cls(
|
||||
name=_clean(raw.get("name")),
|
||||
alias=_clean(raw.get("alias")),
|
||||
@@ -198,7 +198,7 @@ class Prerequis:
|
||||
return not (self.paquets_debian or self.paquets_rhel or self.verifications or self.notes)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any] | None) -> "Prerequis":
|
||||
def from_dict(cls, raw: dict[str, Any] | None) -> Prerequis:
|
||||
raw = raw or {}
|
||||
paquets = raw.get("paquets") or {}
|
||||
return cls(
|
||||
@@ -208,7 +208,7 @@ class Prerequis:
|
||||
notes=str(raw.get("notes") or "").strip(),
|
||||
)
|
||||
|
||||
def fusionner(self, autre: "Prerequis") -> None:
|
||||
def fusionner(self, autre: Prerequis) -> None:
|
||||
for source, cible in (
|
||||
(autre.paquets_debian, self.paquets_debian),
|
||||
(autre.paquets_rhel, self.paquets_rhel),
|
||||
@@ -247,7 +247,7 @@ class Project:
|
||||
return CENTREON_VERSIONS.get(self.centreon_version, {}).get("macro_fields", 5)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any] | None) -> "Project":
|
||||
def from_dict(cls, raw: dict[str, Any] | None) -> Project:
|
||||
raw = raw or {}
|
||||
version = _clean(raw.get("centreon_version")) or VERSION_PAR_DEFAUT
|
||||
version = VERSIONS_HISTORIQUES.get(version, version)
|
||||
@@ -270,7 +270,7 @@ class Project:
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
def merge(self, other: "Project") -> list[str]:
|
||||
def merge(self, other: Project) -> list[str]:
|
||||
"""Ajoute les objets d'un autre projet, en ignorant les noms deja pris."""
|
||||
skipped: list[str] = []
|
||||
|
||||
|
||||
+5
-1
@@ -19,7 +19,11 @@ def _macro(macro) -> Macro:
|
||||
)
|
||||
|
||||
|
||||
def pack_to_project(pack: Pack, only_defaults: bool = True, service_parent: str = "generic-active-service-custom") -> Project:
|
||||
def pack_to_project(
|
||||
pack: Pack,
|
||||
only_defaults: bool = True,
|
||||
service_parent: str = "generic-active-service-custom",
|
||||
) -> Project:
|
||||
"""Construit un projet ne contenant que les objets du pack."""
|
||||
|
||||
project = Project(name=pack.name)
|
||||
|
||||
@@ -14,7 +14,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import timezone
|
||||
from datetime import UTC
|
||||
|
||||
import yaml
|
||||
from flask import Blueprint, jsonify, request
|
||||
@@ -145,7 +145,7 @@ def creer_partage():
|
||||
{
|
||||
"id": partage.id,
|
||||
"url": f"{base}/p/{partage.id}",
|
||||
"expire_le": expire.replace(tzinfo=expire.tzinfo or timezone.utc).isoformat(),
|
||||
"expire_le": expire.replace(tzinfo=expire.tzinfo or UTC).isoformat(),
|
||||
"avertissement": "Les macros de type mot de passe ont ete videes avant l'enregistrement.",
|
||||
}
|
||||
)
|
||||
@@ -194,8 +194,8 @@ def valider_pack(texte: str) -> tuple[dict | None, list[str]]:
|
||||
|
||||
try:
|
||||
pack = Pack.from_dict(brut)
|
||||
except Exception as erreur: # noqa: BLE001 - message rendu tel quel au contributeur
|
||||
return None, problemes + [f"Structure invalide : {erreur}"]
|
||||
except Exception as erreur:
|
||||
return None, [*problemes, f"Structure invalide : {erreur}"]
|
||||
|
||||
if not pack.services:
|
||||
problemes.append("Le pack ne declare aucune sonde.")
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ from sqlalchemy import engine_from_config, pool
|
||||
|
||||
sys.path.insert(0, os.getcwd())
|
||||
|
||||
from enclume.db import Base # noqa: E402
|
||||
from enclume.db import Base
|
||||
|
||||
config = context.config
|
||||
config.set_main_option("sqlalchemy.url", os.environ.get("ENCLUME_DATABASE_URL", ""))
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Outillage de revue. La configuration vit ici pour que la CI et le poste de
|
||||
# developpement appliquent exactement les memes regles.
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 110
|
||||
target-version = "py312"
|
||||
exclude = ["migrations/versions"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "UP", "B", "SIM", "ARG", "RUF"]
|
||||
ignore = [
|
||||
"RUF012", # les attributs de configuration sont volontairement mutables
|
||||
"RUF001", # texte francais sans accents dans le code, par choix
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["ARG005"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q"
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Garde-fous minimaux : ce qui casserait silencieusement le code genere."""
|
||||
|
||||
import json
|
||||
|
||||
from enclume.catalog import Catalog
|
||||
from enclume.generator import build_plan, render_bundle
|
||||
@@ -32,7 +31,7 @@ def test_prefixe_applique_aux_references():
|
||||
projet = projet_exemple()
|
||||
projet.prefix = "ACME-"
|
||||
lignes = [o.import_line() for o in build_plan(projet).ops if o.obj != "#"]
|
||||
liaison = [l for l in lignes if l.startswith("STPL;addhosttemplate")][0]
|
||||
liaison = next(ligne for ligne in lignes if ligne.startswith("STPL;addhosttemplate"))
|
||||
assert liaison.count("ACME-") == 2
|
||||
|
||||
|
||||
@@ -46,13 +45,13 @@ def test_version_historique_convertie():
|
||||
def test_macros_trois_champs_sur_ancienne_version():
|
||||
projet = projet_exemple()
|
||||
projet.centreon_version = "legacy"
|
||||
macro = [o for o in build_plan(projet).ops if o.action == "setmacro"][0]
|
||||
macro = next(o for o in build_plan(projet).ops if o.action == "setmacro")
|
||||
assert len(macro.values) == 3
|
||||
|
||||
|
||||
def test_echappement_shell_des_macros_centreon():
|
||||
fichiers = render_bundle(build_plan(projet_exemple()))
|
||||
ligne = [l for l in fichiers["deploy.sh"].splitlines() if "HOSTADDRESS" in l][0]
|
||||
ligne = next(x for x in fichiers["deploy.sh"].splitlines() if "HOSTADDRESS" in x)
|
||||
assert ligne.startswith("clapi -o CMD -a ADD -v '")
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ ici, et la CI reste sans service externe.
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -28,7 +27,9 @@ services:
|
||||
- name: Queues
|
||||
alias: Queues
|
||||
description: Messages en attente
|
||||
line: "$CENTREONPLUGINS$/centreon_rabbitmq_restapi.pl --plugin=apps::rabbitmq::restapi::plugin --mode=queues --hostname=$HOSTADDRESS$"
|
||||
line: >-
|
||||
$CENTREONPLUGINS$/centreon_rabbitmq_restapi.pl
|
||||
--plugin=apps::rabbitmq::restapi::plugin --mode=queues --hostname=$HOSTADDRESS$
|
||||
macros:
|
||||
- {name: WARNING, value: "1000", description: "Seuil warning"}
|
||||
"""
|
||||
@@ -41,7 +42,11 @@ def app(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("ENCLUME_SECRET_KEY", "cle-de-test")
|
||||
monkeypatch.setenv("ENCLUME_COOKIE_SECURE", "0")
|
||||
|
||||
import enclume.config, enclume.db, enclume.auth, enclume.routes_donnees, enclume.webapp
|
||||
import enclume.auth
|
||||
import enclume.config
|
||||
import enclume.db
|
||||
import enclume.routes_donnees
|
||||
import enclume.webapp
|
||||
|
||||
for module in (enclume.config, enclume.db, enclume.auth, enclume.routes_donnees, enclume.webapp):
|
||||
importlib.reload(module)
|
||||
@@ -276,7 +281,7 @@ def test_page_de_partage_sert_l_editeur(app):
|
||||
def test_moderation_demande_la_connexion(app):
|
||||
page = app.test_client().get("/moderation")
|
||||
assert page.status_code == 200
|
||||
assert "Connexion requise".encode() in page.data
|
||||
assert b"Connexion requise" in page.data
|
||||
|
||||
|
||||
def test_url_de_base_encode_les_caracteres_speciaux(monkeypatch):
|
||||
|
||||
Reference in new issue
Block a user