Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
357cf996ed | ||
|
|
fca36649f9 | ||
|
|
0db69fd023 |
@@ -0,0 +1,52 @@
|
||||
name: Infra
|
||||
|
||||
# Pourquoi : le Terraform du dépôt est resté cassé sans que rien ne le dise, faute de job qui le
|
||||
# joue. Ce workflow n'applique rien : il vérifie le formatage et la validité de chaque racine.
|
||||
# Piège : la boucle parcourt `environments/*`, pour qu'une racine ajoutée soit couverte sans
|
||||
# toucher à ce fichier.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "infra/terraform/**"
|
||||
- ".github/workflows/infra.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "infra/terraform/**"
|
||||
- ".github/workflows/infra.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: infra-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
name: Formatage et validation Terraform
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Récupère le dépôt
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Action tierce, donc epinglee sur un SHA de commit et pas sur un tag mobile : un tag se
|
||||
# redeplace, et ce workflow tourne avec les droits du depot (regle Sonar githubactions:S7637).
|
||||
- name: Installe Terraform
|
||||
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||
with:
|
||||
terraform_version: 1.16.3
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Vérifie le formatage
|
||||
run: terraform fmt -check -recursive infra/terraform
|
||||
|
||||
- name: Valide chaque racine
|
||||
run: |
|
||||
for racine in infra/terraform/environments/*/; do
|
||||
echo "::group::${racine}"
|
||||
terraform -chdir="${racine}" init -backend=false -input=false
|
||||
terraform -chdir="${racine}" validate
|
||||
echo "::endgroup::"
|
||||
done
|
||||
@@ -16,3 +16,4 @@
|
||||
| [0007](adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) | Terminaison TLS par un reverse proxy Nginx, en Docker Compose |
|
||||
| [0008](adr/0008-airflow-execute-le-code-du-backend.md) | Airflow exécute le code du backend en sous-processus, dans son propre environnement |
|
||||
| [0009](adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | Deux environnements sur la VM ENI, un projet Compose chacun, déployés par un runner auto-hébergé |
|
||||
| [0010](adr/0010-terraform-provisionne-github-actions-deploie.md) | Terraform provisionne la machine, GitHub Actions déploie l'application |
|
||||
|
||||
@@ -76,6 +76,7 @@ secret.
|
||||
à chaque push : plusieurs minutes par déploiement, acceptable pour la cadence du projet.
|
||||
- `environments/prod` de Terraform reste vide. Le provisionnement de la machine est porté par
|
||||
`scripts/provision-host.sh`, que Terraform pourra appeler par `remote-exec` le jour où une
|
||||
racine visant la VM existera.
|
||||
racine visant la VM existera. Cette racine existe depuis l'[ADR 0010](0010-terraform-provisionne-github-actions-deploie.md),
|
||||
sous le nom `environments/vm-eni`, et `environments/prod` a disparu avec elle.
|
||||
- L'image frontend quitte `dhi.io/nginx`, registre authentifié dont personne n'a l'accès, pour
|
||||
`nginx:1.28-alpine`, la même image que le proxy. Elle n'avait jamais été construite.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# 0010 - Terraform provisionne la machine, GitHub Actions déploie l'application
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-22
|
||||
|
||||
## Contexte
|
||||
|
||||
L'[ADR 0009](0009-deux-environnements-compose-sur-la-vm-eni.md) a posé la livraison : deux
|
||||
projets Compose sur la VM ENI, alignés sur `dev` et sur `main` par un runner auto-hébergé. Elle
|
||||
ne dit pas qui prépare la machine. C'est `scripts/provision-host.sh`, lancé à la main en SSH.
|
||||
|
||||
La grille d'évaluation attend en C22 que l'infrastructure soit « provisionnée via du code
|
||||
(Terraform, Ansible…) ». Le seul Terraform du dépôt installe un cluster k3s que rien ne
|
||||
consomme, qui n'a jamais été appliqué, et dont la racine ne passait même pas `terraform init`
|
||||
depuis que Terraform refuse les provisioners `destroy` dont la connexion lit autre chose que
|
||||
`self`. Sa racine s'appelait `environments/dev`, nom qui laissait croire à un environnement
|
||||
applicatif alors que les deux environnements réels sont `rec` et `prod`, sur la même machine.
|
||||
|
||||
La branche `feat/deploy` (PR #141) proposait la réponse inverse : Terraform construit les
|
||||
images, lance les conteneurs et copie les sources par SSH. La revue a relevé deux racines sur
|
||||
trois qui ne passent pas `terraform validate`, le mot de passe SSH écrit en clair dans le state,
|
||||
un backend lancé sans base ni variables d'environnement, et trois architectures différentes pour
|
||||
trois environnements.
|
||||
|
||||
## Décision
|
||||
|
||||
**Terraform provisionne la machine, GitHub Actions déploie l'application.** La frontière est
|
||||
nette et vérifiable : `infra/terraform/environments/vm-eni` installe Docker et le plugin
|
||||
Compose, exécute `scripts/provision-host.sh`, enregistre le runner. Il ne construit aucune
|
||||
image, ne lance aucun conteneur, et un `apply` n'interrompt pas la stack qui tourne.
|
||||
|
||||
**Le déploiement continu ne change pas.** `deploy.yml` reste le seul chemin de livraison : push
|
||||
sur `dev` ou `main`, alignement du clone, `make stack-up`, sonde `/api/v1/health/ready`.
|
||||
|
||||
**Le Bash reste la mécanique, Terraform devient le point d'entrée.** `provision-host.sh` connaît
|
||||
les deux environnements, leurs ports décalés, leurs secrets et leurs certificats. Le réécrire en
|
||||
HCL créerait une seconde source de vérité qui divergerait au premier changement de port.
|
||||
|
||||
**Aucun secret dans le state.** Authentification SSH par clé seulement, pas de variable de mot
|
||||
de passe. Le jeton d'enregistrement du runner est une variable `sensitive` fournie à l'`apply`,
|
||||
jamais un `trigger` : les `triggers` sont la seule partie d'un `null_resource` que Terraform
|
||||
persiste.
|
||||
|
||||
**Les racines portent le nom de ce qu'elles provisionnent**, pas d'un environnement applicatif :
|
||||
`vm-eni` pour la machine, `k3s-cible` pour le cluster resté en cible. `environments/prod`,
|
||||
dossier vide, disparaît.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Ansible à la place du Bash** : plus idiomatique pour de la configuration de machine, et le
|
||||
jury le reconnaîtrait immédiatement comme de l'IaC. Mais c'est un outil de plus à installer et
|
||||
à faire tourner, pour réécrire un script qui fonctionne, à trois jours du gel technique.
|
||||
- **Provisioners applicatifs de `feat/deploy`** : voir la revue sur #141. Terraform y devenait un
|
||||
orchestrateur concurrent de Compose, sans base de données ni migrations.
|
||||
- **Terraform appelle aussi `make stack-up`** : le premier démarrage serait plus court d'une
|
||||
commande, mais Terraform se mettrait à porter la livraison, que le runner rejoue à chaque
|
||||
push. Deux chemins pour le même acte, c'est précisément ce que #141 montre qu'il ne faut pas.
|
||||
- **k3s tout de suite** : le cluster serait vide, sans manifeste, sans registre et sans stockage
|
||||
persistant. Le module reste, documenté comme cible.
|
||||
- **State Terraform distant** : un seul opérateur, pas d'exécution concurrente. Le backend local
|
||||
suffit, comme pour `k3s-cible`.
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Le premier `apply` exige un jeton d'enregistrement du runner, valable une heure et pour une
|
||||
seule inscription, que seul un administrateur du dépôt peut créer. L'`apply` n'est donc pas
|
||||
rejouable sans intervention humaine, ce qui est acceptable : il ne se joue qu'à l'installation.
|
||||
- L'utilisateur propriétaire de `/srv/enervision` doit exister sur la machine avant l'`apply`.
|
||||
Terraform vérifie et échoue tôt plutôt que de le créer : décider d'un compte système est une
|
||||
décision d'administration, pas un effet de bord de déploiement.
|
||||
- Terraform ne sait rien de l'état de la stack. `terraform plan` ne dira jamais que la recette
|
||||
est tombée ; c'est la sonde de `deploy.yml` qui le dit.
|
||||
- Pas de provisioner `destroy` sur le runner : il imposerait de mettre le chemin de la clé SSH
|
||||
dans le state, et `svc.sh uninstall` ne désinscrit pas le runner côté GitHub. Le retrait reste
|
||||
manuel, depuis les paramètres du dépôt.
|
||||
- C22 cesse de reposer sur `docker-compose.prod.yml` seul. C23 reste porté par les scripts, que
|
||||
Terraform appelle désormais au lieu de les remplacer.
|
||||
@@ -87,10 +87,10 @@ collecteur ne vient le lire.
|
||||
| Frontend | Angular 22, Node 24 | `apps/frontend` | `En cours` | Tableau de bord sur route `/dashboard`, authentification complète (garde de route, intercepteur de jeton), cinq services HTTP, graphiques Chart.js. `stats`/`alerts` sur fixtures, `predictions` branché sur l'API réelle |
|
||||
| Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (`site`, `dataset`, `reading` en hypertable, `prediction`, `alert`, `recommendation`) |
|
||||
| ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement et de scoring (`enervision_ml.train`/`.score`, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via `GET /predictions`, orchestré par Airflow (`ml_train`/`ml_score`). Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Surveillance de dérive (EC06, #44/#45) pas encore construite |
|
||||
| Infra | Docker Compose, Nginx, Terraform, k3s single-node | `infra`, `docker-compose.prod.yml` | `En cours` | Reverse proxy et overlay de déploiement écrits et validés, jamais lancés sur le serveur ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| Infra | Docker Compose, Nginx, Terraform, k3s single-node | `infra`, `docker-compose.prod.yml` | `En cours` | Reverse proxy et overlay de déploiement écrits et validés, jamais lancés sur le serveur ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). Provisionnement de la VM par Terraform, qui installe Docker, prépare les deux environnements et enregistre le runner, jamais appliqué ([ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée |
|
||||
| Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API |
|
||||
| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Quatre DAGs en sous-processus `uv run` : `ml_train`, `ml_score`, `alertes` et `historical_import`. Le DAG historique orchestre `app.etl.historical_import` et charge `dataset`, `site` et `reading`. L'orchestration API Mock reste à compléter dans #15 |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 6 workflows, 18 jobs : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow. Déploiement continu vers la VM ENI écrit par `deploy.yml`, `dev` en recette et `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), mais jamais exécuté : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Détail dans [50-cicd.md](50-cicd.md) |
|
||||
| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 7 workflows, 19 jobs : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow, formatage et validation du Terraform. Déploiement continu vers la VM ENI écrit par `deploy.yml`, `dev` en recette et `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), mais jamais exécuté : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Détail dans [50-cicd.md](50-cicd.md) |
|
||||
|
||||
## Flux bout en bout
|
||||
|
||||
@@ -189,3 +189,4 @@ Elles vivent dans `../adr/`, pas ici.
|
||||
| [0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) | Terminaison TLS par un reverse proxy Nginx, en Docker Compose |
|
||||
| [0008](../adr/0008-airflow-execute-le-code-du-backend.md) | Airflow exécute le code du backend en sous-processus, dans son propre environnement |
|
||||
| [0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | Deux environnements sur la VM ENI, un projet Compose chacun, déployés par un runner auto-hébergé |
|
||||
| [0010](../adr/0010-terraform-provisionne-github-actions-deploie.md) | Terraform provisionne la machine, GitHub Actions déploie l'application |
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Infrastructure
|
||||
|
||||
Trois topologies coexistent et ne servent pas la même chose. Ce document dit laquelle vaut dans
|
||||
quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre elles.
|
||||
Plusieurs topologies coexistent et ne servent pas la même chose. Ce document dit laquelle vaut
|
||||
dans quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre elles.
|
||||
|
||||
| Topologie | Sert à | Statut |
|
||||
|---|---|---|
|
||||
| Docker Compose | Développer et recetter sur le poste | `Fait` |
|
||||
| Docker Compose plus reverse proxy | Déployer sur la machine on-premise | `Fait` |
|
||||
| Deux projets Compose sur la VM ENI, recette et production | Déploiement continu depuis GitHub | `En cours` |
|
||||
| Provisionnement Terraform de la VM | Préparer la machine et enregistrer le runner | `En cours` |
|
||||
| k3s single-node | Cible à terme | `En cours` |
|
||||
|
||||
## Poste de développement
|
||||
@@ -209,10 +210,38 @@ Nginx renvoie vers `https://$host` sans port, c'est-à-dire vers la production.
|
||||
Le déploiement est décrit dans [50-cicd.md](50-cicd.md) : un runner GitHub Actions installé sur
|
||||
la VM aligne le dossier sur la branche poussée et lance `make stack-up`.
|
||||
|
||||
### Provisionnement de la machine
|
||||
|
||||
Statut : `En cours`. Décision et frontière dans
|
||||
l'[ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md) : **Terraform
|
||||
provisionne la machine, GitHub Actions déploie l'application**. La racine
|
||||
`infra/terraform/environments/vm-eni/` fait trois choses, et rien d'autre.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TF as terraform apply
|
||||
participant VM as VM eadl-2025-nantes-g3
|
||||
participant GH as GitHub
|
||||
|
||||
TF->>VM: SSH, get.docker.com puis docker compose version
|
||||
TF->>VM: copie et exécute scripts/provision-host.sh
|
||||
VM->>VM: deux clones, deux .env, deux certificats
|
||||
TF->>VM: installe actions-runner, config.sh, svc.sh
|
||||
VM->>GH: le runner s'enregistre avec le label eni-g3
|
||||
```
|
||||
|
||||
Aucune image n'y est construite, aucun conteneur lancé : un `apply` n'interrompt pas la stack qui
|
||||
tourne. Le premier démarrage reste manuel, `make stack-up` dans chaque dossier ; les suivants
|
||||
sont joués par le runner à chaque push. Terraform ne sait rien de l'état de la stack, c'est la
|
||||
sonde de `deploy.yml` qui le dit.
|
||||
|
||||
Le jeton d'enregistrement du runner est valable une heure et ne vaut que pour une inscription :
|
||||
l'`apply` n'est pas rejouable sans qu'un administrateur du dépôt en crée un nouveau.
|
||||
|
||||
## Cible à terme, k3s
|
||||
|
||||
Statut : `En cours`. Le module `infra/terraform/modules/k3s/` installe le cluster. Il n'a jamais
|
||||
été appliqué.
|
||||
Statut : `En cours`. Le module `infra/terraform/modules/k3s/` installe le cluster, depuis la
|
||||
racine `infra/terraform/environments/k3s-cible/`. Il n'a jamais été appliqué.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -260,11 +289,13 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
|
||||
| `k3s_version` obligatoire, valeur vide refusée | Sans épinglage, `get.k3s.io` installe la dernière version à chaque exécution : le déploiement cesse d'être reproductible | `validation` dans `modules/k3s/variables.tf` |
|
||||
| Traefik désactivé | Le choix d'ingress reste ouvert, on ne veut pas en subir un par défaut | `k3s_disable_components`, défaut `["traefik"]` |
|
||||
| Kubeconfig laissé en `600/root`, lu par `sudo` | `--write-kubeconfig-mode 644` exposerait `cluster-admin` à tout utilisateur local de la machine | Commentaire et `fetch_kubeconfig` dans `modules/k3s/main.tf` |
|
||||
| State Terraform en backend `local` | Un seul opérateur, pas d'exécution concurrente, pas de dépendance à un stockage distant | `environments/dev/versions.tf` |
|
||||
| State Terraform en backend `local` | Un seul opérateur, pas d'exécution concurrente, pas de dépendance à un stockage distant | `versions.tf` de chaque racine |
|
||||
| `.terraform.lock.hcl` versionné | Fige les versions de provider entre contributeurs et future CI | Commentaire dans `.gitignore` |
|
||||
| `*.tfvars` ignoré, `*.tfvars.example` versionné | Les tfvars portent l'adresse du serveur et le chemin de la clé | `.gitignore` |
|
||||
| Désinstallation gérée au `destroy` | `k3s-uninstall.sh` en `on_failure = continue` : un serveur injoignable ne bloque pas le `destroy` | `modules/k3s/main.tf` |
|
||||
| Deux racines, `dev` et `prod` | Séparation des états et des variables par environnement | `environments/` |
|
||||
| Une racine Terraform par machine provisionnée, nommée d'après elle | `environments/dev` laissait croire à un environnement applicatif, alors que `rec` et `prod` vivent sur la même machine et ne sont pas provisionnés par Terraform | `environments/vm-eni`, `environments/k3s-cible` |
|
||||
| Terraform provisionne, GitHub Actions déploie | Deux chemins pour le même acte de livraison, c'est ce que la revue de #141 relève sur la VM | [ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md) |
|
||||
| Connexion SSH par clé, jamais par mot de passe | Une variable de mot de passe finit en clair dans le state, ou dans les `triggers` qui y sont persistés | `environments/vm-eni/variables.tf`, `modules/k3s/main.tf` |
|
||||
| Terminaison TLS par un reverse proxy Nginx en Compose | L'ingress k3s supposait un registre et des manifestes qui n'existent pas, à quatre jours du rendu | `docker-compose.prod.yml`, [ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) |
|
||||
| Certificat auto-signé par défaut, chemin ACME câblé | Aucun domaine public ne résout vers la machine : le défi HTTP-01 ne peut pas aboutir | `scripts/tls-selfsigned.sh`, `infra/proxy/acme-deploy-hook.sh` |
|
||||
| Un projet Compose par environnement, sur la même machine | Une seule VM, et l'isolation par nom de projet ne demande ni cluster ni registre | `.env` de chaque dossier, [ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md) |
|
||||
@@ -304,4 +335,3 @@ question à trancher, avant toute ressource Kubernetes.
|
||||
- **Quel stockage persistant** côté Kubernetes pour PostgreSQL, et si la base tourne dans le
|
||||
cluster ou à côté.
|
||||
- **Quelle stratégie de sauvegarde et de restauration** des données de mesure.
|
||||
- **Que devient `environments/prod/`**, aujourd'hui réduit à un `.gitkeep`.
|
||||
|
||||
@@ -16,6 +16,12 @@ basculer sur `Fait` au premier déploiement vert. Sa limite, nommée ici plutôt
|
||||
soutenance : les images sont construites sur la machine à chaque déploiement, aucun artefact
|
||||
n'est publié puis promu d'un environnement à l'autre.
|
||||
|
||||
Ce que ce workflow ne fait pas, et ne fera pas : préparer la machine. Installation de Docker,
|
||||
clones, `.env`, certificats et enregistrement du runner sont provisionnés par
|
||||
`infra/terraform/environments/vm-eni`
|
||||
([ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md)). Terraform provisionne,
|
||||
GitHub Actions déploie ; aucun des deux ne fait le travail de l'autre.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
```mermaid
|
||||
@@ -45,6 +51,10 @@ flowchart TB
|
||||
ab["image<br/>construction de l'image"]
|
||||
end
|
||||
|
||||
subgraph infw["Infra · infra.yml"]
|
||||
it["terraform<br/>fmt -check, init et validate par racine"]
|
||||
end
|
||||
|
||||
subgraph sq["SonarQube · sonarqube.yml"]
|
||||
sb1["build-front / test-front"]
|
||||
sb2["build-back / test-back"]
|
||||
@@ -57,6 +67,7 @@ flowchart TB
|
||||
push --> fd
|
||||
push --> mv & ms
|
||||
push --> av & ab
|
||||
push --> it
|
||||
push --> sb1 & sb2 --> sscan
|
||||
|
||||
subgraph cd["Déploiement · deploy.yml"]
|
||||
@@ -68,11 +79,11 @@ flowchart TB
|
||||
|
||||
## Déclenchement
|
||||
|
||||
Les cinq workflows se déclenchent sur `push` **et** sur `pull_request`, filtrés par **chemin** :
|
||||
`backend.yml` sur `apps/backend/**`, `frontend.yml` sur `apps/frontend/**`, `ml.yml` sur `ml/**`,
|
||||
`airflow.yml` sur `etl/airflow/**` **plus des chemins de `ml/` et de `apps/backend/`**, chacun
|
||||
incluant son propre fichier de workflow dans le filtre pour qu'une modification du pipeline
|
||||
déclenche le pipeline.
|
||||
Les six workflows hébergés par GitHub se déclenchent sur `push` **et** sur `pull_request`,
|
||||
filtrés par **chemin** : `backend.yml` sur `apps/backend/**`, `frontend.yml` sur
|
||||
`apps/frontend/**`, `ml.yml` sur `ml/**`, `infra.yml` sur `infra/terraform/**`, `airflow.yml` sur
|
||||
`etl/airflow/**` **plus des chemins de `ml/` et de `apps/backend/`**, chacun incluant son propre
|
||||
fichier de workflow dans le filtre pour qu'une modification du pipeline déclenche le pipeline.
|
||||
|
||||
Le filtre d'`airflow.yml` mérite un mot : il inclut `ml/pyproject.toml`, `ml/uv.lock`,
|
||||
`ml/enervision_ml/**`, `apps/backend/pyproject.toml`, `apps/backend/uv.lock` et
|
||||
@@ -97,7 +108,7 @@ environnement.
|
||||
|
||||
## Déploiement
|
||||
|
||||
`deploy.yml` est le sixième workflow, et le seul qui ne tourne pas chez GitHub : il s'exécute sur
|
||||
`deploy.yml` est le septième workflow, et le seul qui ne tourne pas chez GitHub : il s'exécute sur
|
||||
un runner auto-hébergé installé sur la VM ENI, label `eni-g3`, parce que les runners hébergés ne
|
||||
joignent pas une adresse privée d'école. Le runner se connecte en sortie vers GitHub, aucun port
|
||||
entrant n'est ouvert.
|
||||
@@ -151,6 +162,7 @@ dans [10-infra.md](10-infra.md).
|
||||
| Build `npm run build` | frontend | compilation | Bloque |
|
||||
| Intégrité des DAGs | airflow | chargement des DAGs sans erreur d'import | Bloque |
|
||||
| Construction de l'image Airflow | airflow | `docker build` de `etl/airflow/Dockerfile` | Bloque |
|
||||
| Formatage et validité Terraform | infra | `fmt -check -recursive`, puis `init` et `validate` par racine | Bloque |
|
||||
|
||||
Deux seuils portent une décision qu'il faut savoir défendre :
|
||||
|
||||
|
||||
+36
-6
@@ -1,18 +1,48 @@
|
||||
# Infrastructure
|
||||
|
||||
Provisionnement Terraform de la machine on-premise (serveur physique, accessible en SSH).
|
||||
Provisionnement Terraform des machines on-premise. Terraform prepare la machine, GitHub Actions
|
||||
deploie l'application : voir l'[ADR 0010](../docs/adr/0010-terraform-provisionne-github-actions-deploie.md).
|
||||
Rien ici ne construit d'image ni ne lance de conteneur.
|
||||
|
||||
- `terraform/modules` : modules reutilisables.
|
||||
- `k3s` : installe un cluster k3s single-node sur une machine distante via SSH
|
||||
(script officiel `get.k3s.io`) et rapatrie le kubeconfig en local.
|
||||
- `terraform/environments/<env>` : racines Terraform, une par environnement.
|
||||
- `dev` : instancie le module `k3s` sur le serveur de l'ecole.
|
||||
- `prod` : non initialise, voir le ticket dedie.
|
||||
- `terraform/environments/<racine>` : une racine par machine provisionnee.
|
||||
- `vm-eni` : la VM `eadl-2025-nantes-g3`, qui porte les environnements `rec` et `prod`
|
||||
([ADR 0009](../docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Installe Docker,
|
||||
execute `scripts/provision-host.sh`, enregistre le runner GitHub Actions.
|
||||
- `k3s-cible` : le cluster k3s, cible a terme de `docs/architecture/10-infra.md`. Jamais
|
||||
applique.
|
||||
|
||||
## Usage (environments/dev)
|
||||
## Usage (environments/vm-eni)
|
||||
|
||||
```bash
|
||||
cd infra/terraform/environments/dev
|
||||
cd infra/terraform/environments/vm-eni
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
|
||||
`terraform.tfvars` est ignore par git. Trois valeurs sont a renseigner avant l'apply :
|
||||
|
||||
- `proprietaire` : l'utilisateur qui possede `/srv/enervision` et fait tourner le runner. Il doit
|
||||
deja exister sur la machine.
|
||||
- `runner_version` : a epingler depuis <https://github.com/actions/runner/releases>.
|
||||
- `runner_token` : jeton d'enregistrement, valable une heure et pour une seule inscription.
|
||||
Parametres du depot, Actions, Runners, New self-hosted runner. Seul un administrateur du depot
|
||||
peut le creer.
|
||||
|
||||
Apres l'apply, la machine porte `/srv/enervision/rec` et `/srv/enervision/prod`, chacun avec son
|
||||
`.env` et son certificat. Le premier demarrage reste manuel, `make stack-up` dans chaque dossier ;
|
||||
les suivants sont joues par le runner a chaque push sur `dev` et sur `main`.
|
||||
|
||||
Retirer le runner se fait a la main, depuis les parametres du depot : `terraform destroy` ne le
|
||||
desinscrit pas.
|
||||
|
||||
## Usage (environments/k3s-cible)
|
||||
|
||||
```bash
|
||||
cd infra/terraform/environments/k3s-cible
|
||||
cp terraform.tfvars.example terraform.tfvars # renseigner ssh_host / ssh_private_key_path
|
||||
terraform init
|
||||
terraform apply
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/null" {
|
||||
version = "3.3.2"
|
||||
constraints = "~> 3.2"
|
||||
hashes = [
|
||||
"h1:IQ1qrkht1sC1nibUR+AJ3ulryyhVDHfCHZhoJi0sg2Y=",
|
||||
"zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08",
|
||||
"zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55",
|
||||
"zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27",
|
||||
"zh:64a8f9ce7852d9efc5b464c12306c946366d59f5e2757def97969c9fd64bd1d6",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:92a374fb736a52f465283326d0a5bf4f495132eb99be209dfb4c75ec803fe8db",
|
||||
"zh:98da9c42785d27a50f0604758bcb61a30f6278b9f2acd92bb3b2046e0e71916c",
|
||||
"zh:b0f7896fae554729cdf4a24ac06359a050cff5817e6cd8597cba8a4ae01a7409",
|
||||
"zh:bc8179ee35d67c72fb03012e7023b9f9816f033a7ec4109c001dd6d29752e812",
|
||||
"zh:d23a598f713bfb6098bc003571d7de90b5a33b78f9be240488252fe5f3c2a60d",
|
||||
"zh:d2855b922ea345dbd89ea287e4c6c4757e38bc0aaffeb2b79aa0b8004f9c53ff",
|
||||
"zh:d3a60422bc6a2f9244d076c5222c07060c826ef91bdbaf4634cb752b86057473",
|
||||
"zh:faa01928c25d2a6ecd9c7eb8b88134cb08de55a6b11ca6c703ac0092845344ba",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/null" {
|
||||
version = "3.3.2"
|
||||
constraints = "~> 3.2"
|
||||
hashes = [
|
||||
"h1:IQ1qrkht1sC1nibUR+AJ3ulryyhVDHfCHZhoJi0sg2Y=",
|
||||
"zh:10ec43b8b7b18d5639238c7fb9e111f6a4b038523dd66c7a426bf27b25fa4c08",
|
||||
"zh:60beb9cc2ad5b871c710860cee75b42850cc6acd43db0d77cb5e00fda7288b55",
|
||||
"zh:62538582d0a4a2f10ad8a8d9a6c3cd3f05af6c6d91c6641ffc78d4f0e8e69b27",
|
||||
"zh:64a8f9ce7852d9efc5b464c12306c946366d59f5e2757def97969c9fd64bd1d6",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:92a374fb736a52f465283326d0a5bf4f495132eb99be209dfb4c75ec803fe8db",
|
||||
"zh:98da9c42785d27a50f0604758bcb61a30f6278b9f2acd92bb3b2046e0e71916c",
|
||||
"zh:b0f7896fae554729cdf4a24ac06359a050cff5817e6cd8597cba8a4ae01a7409",
|
||||
"zh:bc8179ee35d67c72fb03012e7023b9f9816f033a7ec4109c001dd6d29752e812",
|
||||
"zh:d23a598f713bfb6098bc003571d7de90b5a33b78f9be240488252fe5f3c2a60d",
|
||||
"zh:d2855b922ea345dbd89ea287e4c6c4757e38bc0aaffeb2b79aa0b8004f9c53ff",
|
||||
"zh:d3a60422bc6a2f9244d076c5222c07060c826ef91bdbaf4634cb752b86057473",
|
||||
"zh:faa01928c25d2a6ecd9c7eb8b88134cb08de55a6b11ca6c703ac0092845344ba",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Pourquoi : Terraform provisionne la machine, GitHub Actions la deploie (ADR 0010). Rien ici ne
|
||||
# construit d'image ni ne lance de conteneur : la livraison reste portee par `deploy.yml` et
|
||||
# `make stack-up`, et un `apply` n'interrompt pas la stack qui tourne.
|
||||
# Piege : seul le bloc `triggers` d'un `null_resource` atterrit dans le state. Ni le jeton du
|
||||
# runner ni la cle SSH n'y figurent, et ne doivent jamais y etre ajoutes pour forcer un rejeu.
|
||||
# Contrainte : pas de provisioner `destroy` sur le runner. Il imposerait une connexion ne lisant
|
||||
# que `self`, donc le chemin de la cle SSH dans le state, et `svc.sh uninstall` ne desinscrit pas
|
||||
# le runner cote GitHub : le retrait reste manuel, depuis les parametres du depot.
|
||||
# Ref : ADR 0009 pour les deux environnements, `scripts/provision-host.sh` pour leur contenu.
|
||||
|
||||
locals {
|
||||
sudo = var.ssh_user == "root" ? "" : "sudo "
|
||||
en_tant_que = "${var.ssh_user == "root" ? "" : "sudo "}runuser -u ${var.proprietaire} --"
|
||||
provisionneur = "${path.root}/../../../../scripts/provision-host.sh"
|
||||
runner_archive = "actions-runner-linux-x64-${var.runner_version}.tar.gz"
|
||||
# Substitution shell, evaluee par le sh -c distant : un nom de runner doit etre unique dans
|
||||
# le depot, le nom d'hote l'est deja et le reste si cette racine sert a une autre machine.
|
||||
runner_nom = var.runner_nom != "" ? var.runner_nom : "$(hostname -s)"
|
||||
}
|
||||
|
||||
resource "null_resource" "docker_engine" {
|
||||
triggers = {
|
||||
hote = var.ssh_host
|
||||
user = var.proprietaire
|
||||
}
|
||||
|
||||
connection {
|
||||
type = "ssh"
|
||||
host = var.ssh_host
|
||||
port = var.ssh_port
|
||||
user = var.ssh_user
|
||||
private_key = file(pathexpand(var.ssh_private_key_path))
|
||||
timeout = "5m"
|
||||
}
|
||||
|
||||
provisioner "remote-exec" {
|
||||
inline = [
|
||||
<<-EOT
|
||||
set -eu
|
||||
id ${var.proprietaire} >/dev/null 2>&1 || {
|
||||
echo "l'utilisateur ${var.proprietaire} n'existe pas sur la machine" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v docker >/dev/null || ${local.sudo}sh -c 'curl -fsSL https://get.docker.com | sh'
|
||||
${local.sudo}systemctl enable --now docker
|
||||
${local.sudo}usermod -aG docker ${var.proprietaire}
|
||||
${local.sudo}docker compose version
|
||||
EOT
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# `provision-host.sh` verifie lui-meme docker, compose et la sortie HTTPS, puis prepare un clone
|
||||
# par environnement, son `.env` et son certificat. Il est rejouable : un `.env` existant n'est
|
||||
# jamais reecrit, un certificat present jamais regenere.
|
||||
resource "null_resource" "environnements" {
|
||||
depends_on = [null_resource.docker_engine]
|
||||
|
||||
triggers = {
|
||||
script = filesha256(local.provisionneur)
|
||||
racine = var.racine
|
||||
depot = var.depot_url
|
||||
}
|
||||
|
||||
connection {
|
||||
type = "ssh"
|
||||
host = var.ssh_host
|
||||
port = var.ssh_port
|
||||
user = var.ssh_user
|
||||
private_key = file(pathexpand(var.ssh_private_key_path))
|
||||
timeout = "5m"
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
source = local.provisionneur
|
||||
destination = "/tmp/provision-host.sh"
|
||||
}
|
||||
|
||||
provisioner "remote-exec" {
|
||||
inline = [
|
||||
<<-EOT
|
||||
set -eu
|
||||
${local.sudo}env RACINE='${var.racine}' \
|
||||
REPO_URL='${var.depot_url}' \
|
||||
PROPRIETAIRE='${var.proprietaire}' \
|
||||
PUBLIC_IP='${var.adresse_publique}' \
|
||||
bash /tmp/provision-host.sh
|
||||
rm -f /tmp/provision-host.sh
|
||||
EOT
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Piege : le jeton d'enregistrement expire en une heure. Un `apply` rejoue cette ressource des
|
||||
# que `runner_version`, `runner_labels` ou `runner_nom` change, et redemande donc un jeton frais.
|
||||
resource "null_resource" "runner_github" {
|
||||
depends_on = [null_resource.environnements]
|
||||
|
||||
triggers = {
|
||||
version = var.runner_version
|
||||
labels = var.runner_labels
|
||||
nom = local.runner_nom
|
||||
dossier = var.runner_dossier
|
||||
}
|
||||
|
||||
connection {
|
||||
type = "ssh"
|
||||
host = var.ssh_host
|
||||
port = var.ssh_port
|
||||
user = var.ssh_user
|
||||
private_key = file(pathexpand(var.ssh_private_key_path))
|
||||
timeout = "5m"
|
||||
}
|
||||
|
||||
provisioner "remote-exec" {
|
||||
inline = [
|
||||
<<-EOT
|
||||
set -eu
|
||||
${local.sudo}install -d -o ${var.proprietaire} -g ${var.proprietaire} ${var.runner_dossier}
|
||||
if [ ! -x ${var.runner_dossier}/config.sh ]; then
|
||||
curl -fsSL -o /tmp/${local.runner_archive} \
|
||||
https://github.com/actions/runner/releases/download/v${var.runner_version}/${local.runner_archive}
|
||||
${local.sudo}tar -xzf /tmp/${local.runner_archive} -C ${var.runner_dossier}
|
||||
${local.sudo}chown -R ${var.proprietaire}:${var.proprietaire} ${var.runner_dossier}
|
||||
rm -f /tmp/${local.runner_archive}
|
||||
fi
|
||||
if [ ! -f ${var.runner_dossier}/.runner ]; then
|
||||
${local.en_tant_que} sh -c 'cd ${var.runner_dossier} && ./config.sh --unattended --replace \
|
||||
--url ${var.runner_url} --token ${var.runner_token} \
|
||||
--labels ${var.runner_labels} --name ${local.runner_nom} --work _work'
|
||||
${local.sudo}${var.runner_dossier}/svc.sh install ${var.proprietaire}
|
||||
fi
|
||||
${local.sudo}${var.runner_dossier}/svc.sh start
|
||||
EOT
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "machine" {
|
||||
description = "Machine provisionnee et racine qui porte un clone par environnement."
|
||||
value = "${var.ssh_user}@${var.ssh_host}:${var.racine}"
|
||||
}
|
||||
|
||||
output "runner" {
|
||||
description = "Dossier d'installation du runner et libelles supplementaires annonces a GitHub."
|
||||
value = "${var.runner_dossier} (${var.runner_labels})"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
ssh_host = "10.101.200.37"
|
||||
ssh_port = 22
|
||||
ssh_user = "root"
|
||||
ssh_private_key_path = "~/.ssh/id_ed25519"
|
||||
|
||||
# Utilisateur qui possede la racine et fait tourner le runner. Il doit deja exister sur la
|
||||
# machine : git refuse les depots appartenant a un autre utilisateur, et un .env en 600 lui
|
||||
# echapperait.
|
||||
proprietaire = "enervision"
|
||||
|
||||
# Epingler une version reelle : https://github.com/actions/runner/releases
|
||||
runner_version = "2.330.0"
|
||||
|
||||
# Jeton d'enregistrement du runner, valable une heure et pour une seule inscription :
|
||||
# Parametres du depot > Actions > Runners > New self-hosted runner. Seul un administrateur du
|
||||
# depot peut le creer. terraform.tfvars est ignore par git, mais le jeton ne doit pas y rester
|
||||
# apres l'apply.
|
||||
runner_token = "A_RENSEIGNER"
|
||||
|
||||
# Nom du runner cote GitHub. Vide par defaut : le nom d'hote de la machine. A renseigner
|
||||
# seulement si deux runners doivent tourner sur la meme machine, leurs noms devant differer.
|
||||
# runner_nom = "eni-g3-bis"
|
||||
@@ -0,0 +1,90 @@
|
||||
variable "ssh_host" {
|
||||
type = string
|
||||
description = "Adresse de la VM ENI qui porte les deux environnements (ADR 0009)."
|
||||
}
|
||||
|
||||
variable "ssh_port" {
|
||||
type = number
|
||||
description = "Port SSH de la VM."
|
||||
default = 22
|
||||
}
|
||||
|
||||
variable "ssh_user" {
|
||||
type = string
|
||||
description = "Utilisateur SSH du provisionnement. Different de root, les commandes privilegiees sont prefixees par sudo."
|
||||
default = "root"
|
||||
}
|
||||
|
||||
variable "ssh_private_key_path" {
|
||||
type = string
|
||||
description = "Chemin local vers la cle privee SSH. L'authentification par mot de passe n'est volontairement pas prise en charge : une variable de mot de passe finit en clair dans le state ou dans les triggers."
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proprietaire" {
|
||||
type = string
|
||||
description = "Utilisateur qui possede la racine et fait tourner le runner. Il doit exister sur la machine : git refuse les depots appartenant a un autre utilisateur, et un .env en 600 lui echapperait."
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[a-z_][a-z0-9_-]*$", var.proprietaire))
|
||||
error_message = "proprietaire doit etre un nom d'utilisateur Unix valide."
|
||||
}
|
||||
}
|
||||
|
||||
variable "racine" {
|
||||
type = string
|
||||
description = "Dossier qui porte un clone du depot par environnement."
|
||||
default = "/srv/enervision"
|
||||
}
|
||||
|
||||
variable "depot_url" {
|
||||
type = string
|
||||
description = "URL de clonage du depot, passee a provision-host.sh."
|
||||
default = "https://github.com/ineszang/ProjetPiscine_EnerVision.git"
|
||||
}
|
||||
|
||||
variable "adresse_publique" {
|
||||
type = string
|
||||
description = "Adresse annoncee dans les certificats auto-signes. Vide : la premiere adresse de la VM."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "runner_url" {
|
||||
type = string
|
||||
description = "Depot GitHub auquel le runner s'enregistre."
|
||||
default = "https://github.com/ineszang/ProjetPiscine_EnerVision"
|
||||
}
|
||||
|
||||
variable "runner_version" {
|
||||
type = string
|
||||
description = "Version d'actions-runner a installer, sans le v initial (ex: 2.330.0). Voir https://github.com/actions/runner/releases."
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.runner_version))
|
||||
error_message = "runner_version doit etre epinglee explicitement (ex: 2.330.0), sinon l'installation cesse d'etre reproductible."
|
||||
}
|
||||
}
|
||||
|
||||
variable "runner_token" {
|
||||
type = string
|
||||
description = "Jeton d'enregistrement du runner. Expire au bout d'une heure et ne vaut que pour une inscription : Parametres du depot > Actions > Runners > New self-hosted runner. Seul un administrateur du depot peut le creer."
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "runner_labels" {
|
||||
type = string
|
||||
description = "Libelles supplementaires du runner. deploy.yml cible [self-hosted, linux, eni-g3], les deux premiers etant poses par GitHub."
|
||||
default = "eni-g3"
|
||||
}
|
||||
|
||||
variable "runner_nom" {
|
||||
type = string
|
||||
description = "Nom du runner cote GitHub, unique dans le depot. Vide : le nom d'hote de la machine, qui reste unique si cette racine est reprise pour une seconde VM. A renseigner pour faire tourner deux runners sur la meme machine."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "runner_dossier" {
|
||||
type = string
|
||||
description = "Dossier d'installation du runner sur la machine."
|
||||
default = "/opt/actions-runner"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
terraform {
|
||||
required_version = ">= 1.7"
|
||||
|
||||
required_providers {
|
||||
null = {
|
||||
source = "hashicorp/null"
|
||||
version = "~> 3.2"
|
||||
}
|
||||
}
|
||||
|
||||
backend "local" {
|
||||
path = "terraform.tfstate"
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,26 @@ locals {
|
||||
kubeconfig_cmd = "${local.sudo_prefix}cat /etc/rancher/k3s/k3s.yaml"
|
||||
}
|
||||
|
||||
# Piege : un provisioner `destroy` impose que tout le bloc `connection` ne lise que `self`, sinon
|
||||
# `terraform init` refuse le module. D'ou la connexion batie sur `triggers`, ou ne figurent que
|
||||
# l'adresse, le port, l'utilisateur et le chemin de la cle : jamais la cle ni un mot de passe.
|
||||
resource "null_resource" "k3s_install" {
|
||||
triggers = {
|
||||
ssh_host = var.ssh_host
|
||||
k3s_version = var.k3s_version
|
||||
disable_components = join(",", var.k3s_disable_components)
|
||||
ssh_host = var.ssh_host
|
||||
ssh_port = tostring(var.ssh_port)
|
||||
ssh_user = var.ssh_user
|
||||
ssh_key_path = var.ssh_private_key_path
|
||||
sudo_prefix = local.sudo_prefix
|
||||
k3s_version = var.k3s_version
|
||||
disable_components = join(",", var.k3s_disable_components)
|
||||
}
|
||||
|
||||
connection {
|
||||
type = "ssh"
|
||||
host = var.ssh_host
|
||||
port = var.ssh_port
|
||||
user = var.ssh_user
|
||||
private_key = file(var.ssh_private_key_path)
|
||||
host = self.triggers.ssh_host
|
||||
port = tonumber(self.triggers.ssh_port)
|
||||
user = self.triggers.ssh_user
|
||||
private_key = file(pathexpand(self.triggers.ssh_key_path))
|
||||
}
|
||||
|
||||
provisioner "remote-exec" {
|
||||
@@ -33,7 +40,7 @@ resource "null_resource" "k3s_install" {
|
||||
when = destroy
|
||||
on_failure = continue
|
||||
inline = [
|
||||
"${local.sudo_prefix}sh -c 'test -x /usr/local/bin/k3s-uninstall.sh && /usr/local/bin/k3s-uninstall.sh || true'",
|
||||
"${self.triggers.sudo_prefix}sh -c 'test -x /usr/local/bin/k3s-uninstall.sh && /usr/local/bin/k3s-uninstall.sh || true'",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user