This commit is contained in:
alessandro barucci
2026-09-12 19:21:41 +02:00
parent 58a443a076
commit f8ffa9e065
19 changed files with 1275 additions and 3 deletions
+85
View File
@@ -0,0 +1,85 @@
# gitops-repo - esempio completo (3 namespace, Fleet su RKE2)
Repo di esempio pronto da usare come base: GitOps limitato a
namespace-A, namespace-B, namespace-C, con Fleet come motore di sync.
## Struttura
```
.
├── bootstrap/ <- file legacy Argo CD (non usati con Fleet)
│ ├── 00-namespace.yaml
│ ├── 01-appproject.yaml
│ └── 02-root.yaml
│
└── clusters/
└── idc/ <- nome cluster/repository scope attuale
├── namespace-A/
│ ├── application.yaml <- da migrare a Fleet GitRepo
│ └── manifests/
│ └── deployment.yaml
├── namespace-B/
│ ├── application.yaml <- da migrare a Fleet GitRepo
│ └── manifests/
│ └── deployment.yaml
└── namespace-C/
├── application.yaml <- gia convertito a Fleet GitRepo
└── manifests/
└── deployment.yaml
```
## Cosa applichi a mano con Fleet
- Applichi a mano i manifest Fleet di tipo GitRepo (uno per namespace o uno aggregato).
- I file in clusters/idc/namespace-*/manifests vengono sincronizzati automaticamente da Fleet.
- I file in bootstrap sono legacy Argo CD: con Fleet installato, non sono necessari.
## Bootstrap Fleet - passo per passo
### 1. Verifica Fleet
```bash
kubectl -n cattle-fleet-system get pods
kubectl get crd gitrepos.fleet.cattle.io
```
### 2. Applica il GitRepo
Esempio con namespace-C:
```bash
kubectl apply -f clusters/idc/namespace-C/application.yaml
```
### 3. Verifica stato Fleet
```bash
kubectl -n fleet-local get gitrepo
kubectl -n cattle-fleet-system get bundles
kubectl -n cattle-fleet-system get bundledeployments
```
## Repo privato GitHub
Se il repo e privato, configura le credenziali per Fleet (Secret nel namespace Fleet usato, tipicamente fleet-local o fleet-default).
## Prima di usare questo repo
Sostituisci ovunque compare:
- <tua-org> -> org/utente reale del tuo repo Git
- i deployment di esempio -> i tuoi manifest reali
## Migrazione consigliata (A e B)
Attualmente solo namespace-C e gia in formato Fleet.
Per allineare tutto:
1. Converti clusters/idc/namespace-A/application.yaml in GitRepo Fleet.
2. Converti clusters/idc/namespace-B/application.yaml in GitRepo Fleet.
3. Applica i due manifest e verifica bundle/bundledeployments.
## Estendere a un quarto namespace in futuro
1. Crea clusters/idc/namespace-D/manifests con i tuoi YAML.
2. Crea clusters/idc/namespace-D/application.yaml in formato Fleet GitRepo.
3. Applica il manifest e verifica la generazione dei bundle.
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: argocd
labels:
app.kubernetes.io/name: argocd
+37
View File
@@ -0,0 +1,37 @@
# =====================================================================
# AppProject con perimetro ristretto a namespace-A, namespace-B,
# namespace-C. Qualunque Application che referenzi questo project e
# provi a scrivere in un namespace diverso da questi (+ argocd, per le
# risorse interne) viene rifiutata da Argo CD stesso.
# =====================================================================
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: gitops-3ns
namespace: argocd
spec:
description: "GitOps limitato a namespace-A, namespace-B, namespace-C"
sourceRepos:
- "https://github.com/<tua-org>/gitops-repo.git"
destinations:
- namespace: argocd
server: "https://kubernetes.default.svc"
- namespace: namespace-A
server: "https://kubernetes.default.svc"
- namespace: namespace-B
server: "https://kubernetes.default.svc"
- namespace: namespace-C
server: "https://kubernetes.default.svc"
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceWhitelist:
- group: "*"
kind: "*"
orphanedResources:
warn: true
+40
View File
@@ -0,0 +1,40 @@
# =====================================================================
# Root "app of apps": punta a gitops/clusters/idc/ nel repo. Grazie a
# directory.recurse=true, scopre automaticamente ogni application.yaml
# trovato dentro namespace-A/, namespace-B/, namespace-C/ e li applica
# come proprie Application figlie.
# =====================================================================
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-3ns
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc
directory:
recurse: true
destination:
server: "https://kubernetes.default.svc"
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 10s
factor: 2
maxDuration: 3m
@@ -0,0 +1,24 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: namespace-a
namespace: argocd
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc/namespace-A/manifests
destination:
server: "https://kubernetes.default.svc"
namespace: namespace-A
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
@@ -0,0 +1,37 @@
# =====================================================================
# Esempio di manifest reale — sostituiscilo con le tue risorse
# effettive. Ogni file .yaml in questa cartella viene applicato
# automaticamente da Argo CD (Application "namespace-a").
# =====================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: esempio-app-a
namespace: namespace-A
spec:
replicas: 1
selector:
matchLabels:
app: esempio-app-a
template:
metadata:
labels:
app: esempio-app-a
spec:
containers:
- name: esempio-app-a
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: esempio-app-a
namespace: namespace-A
spec:
selector:
app: esempio-app-a
ports:
- port: 80
targetPort: 80
@@ -0,0 +1,24 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: namespace-b
namespace: argocd
spec:
project: gitops-3ns
source:
repoURL: "https://github.com/<tua-org>/gitops-repo.git"
targetRevision: main
path: gitops/clusters/idc/namespace-B/manifests
destination:
server: "https://kubernetes.default.svc"
namespace: namespace-B
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: esempio-app-b
namespace: namespace-B
spec:
replicas: 1
selector:
matchLabels:
app: esempio-app-b
template:
metadata:
labels:
app: esempio-app-b
spec:
containers:
- name: esempio-app-b
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: esempio-app-b
namespace: namespace-B
spec:
selector:
app: esempio-app-b
ports:
- port: 80
targetPort: 80
@@ -0,0 +1,13 @@
apiVersion: fleet.cattle.io/v1alpha1
kind: GitRepo
metadata:
name: namespace-c
namespace: fleet-local
spec:
repo: "https://github.com/<tua-org>/gitops-repo.git"
branch: main
paths:
- gitops/clusters/idc/namespace-C/manifests
targets:
- clusterName: local
namespace: namespace-C
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: esempio-app-c
namespace: namespace-C
spec:
replicas: 1
selector:
matchLabels:
app: esempio-app-c
template:
metadata:
labels:
app: esempio-app-c
spec:
containers:
- name: esempio-app-c
image: nginx:1.27
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: esempio-app-c
namespace: namespace-C
spec:
selector:
app: esempio-app-c
ports:
- port: 80
targetPort: 80
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env bash
# =====================================================================
# extract-manifests.sh
#
# Estrae tutte le risorse namespaced dai namespace indicati, le ripulisce
# con kubectl-neat (rimuove resourceVersion, uid, status, managedFields,
# creationTimestamp, ecc.) e le salva organizzate per namespace/kind,
# pronte per essere committate in un repo GitOps.
#
# Uso:
# ./extract-manifests.sh [opzioni] <namespace1> <namespace2> ...
# ./extract-manifests.sh [opzioni] --namespaces-file namespaces.txt
#
# Opzioni:
# -o, --output <dir> Directory di output (default: ./export)
# -s, --include-secrets Include anche i Secret (in CHIARO, base64 non
# cifrato — vedi warning qui sotto). Di default
# i Secret vengono SALTATI per sicurezza.
# -f, --namespaces-file File con un namespace per riga. Righe vuote e
# righe che iniziano con # vengono ignorate.
# -k, --kinds <lista> Lista custom di kind separati da virgola,
# al posto della discovery automatica
# (es. "deployment,service,httproute")
# -h, --help Mostra questo help
#
# Esempi:
# ./extract-manifests.sh monitoring minio idcidp-dev
# ./extract-manifests.sh -o ./gitops-repo/imported -s monitoring
# ./extract-manifests.sh -k "httproute,gateway" nginx-gateway
# ./extract-manifests.sh -f ./namespaces.txt -o ./gitops-repo/imported
#
# Prerequisiti:
# - kubectl configurato e puntato al cluster corretto
# - kubectl-neat installato (kubectl krew install neat)
# https://github.com/itaysk/kubectl-neat
# =====================================================================
set -euo pipefail
# ---------------------------------------------------------------------
# Default
# ---------------------------------------------------------------------
OUTPUT_DIR="./export"
INCLUDE_SECRETS="false"
CUSTOM_KINDS=""
NAMESPACES_FILE=""
# Kind che non ha senso portare in un repo GitOps: generati/gestiti
# automaticamente da controller, non sono mai "desired state" da
# dichiarare a mano.
EXCLUDED_KINDS="events pods replicasets endpoints endpointslices controllerrevisions"
usage() {
grep '^#' "$0" | sed -e 's/^#//' -e 's/^ //'
exit 0
}
load_namespaces_from_file() {
local file_path="$1"
if [[ ! -f "$file_path" ]]; then
echo "Errore: file namespace non trovato: $file_path" >&2
exit 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}"
[[ -z "$line" ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" ]] && continue
NAMESPACES+=("$line")
done < "$file_path"
}
# ---------------------------------------------------------------------
# Parsing argomenti
# ---------------------------------------------------------------------
NAMESPACES=()
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-s|--include-secrets)
INCLUDE_SECRETS="true"
shift
;;
-f|--namespaces-file)
NAMESPACES_FILE="$2"
shift 2
;;
-k|--kinds)
CUSTOM_KINDS="$2"
shift 2
;;
-h|--help)
usage
;;
-*)
echo "Opzione sconosciuta: $1" >&2
exit 1
;;
*)
NAMESPACES+=("$1")
shift
;;
esac
done
if [[ -n "$NAMESPACES_FILE" ]]; then
load_namespaces_from_file "$NAMESPACES_FILE"
fi
if [[ ${#NAMESPACES[@]} -eq 0 ]]; then
echo "Errore: specifica almeno un namespace o usa --namespaces-file." >&2
echo "Uso: $0 [opzioni] <namespace1> <namespace2> ..." >&2
echo " o: $0 [opzioni] --namespaces-file namespaces.txt" >&2
exit 1
fi
# ---------------------------------------------------------------------
# Prerequisiti
# ---------------------------------------------------------------------
if ! command -v kubectl >/dev/null 2>&1; then
echo "Errore: kubectl non trovato nel PATH." >&2
exit 1
fi
if ! kubectl neat --help >/dev/null 2>&1; then
echo "Errore: plugin kubectl-neat non trovato." >&2
echo "Installa con: kubectl krew install neat" >&2
echo "(krew: https://krew.sigs.k8s.io/docs/user-guide/setup/install/)" >&2
exit 1
fi
mkdir -p "$OUTPUT_DIR"
echo "==> Output directory: $OUTPUT_DIR"
echo "==> Namespace da processare: ${NAMESPACES[*]}"
echo "==> Include Secret: $INCLUDE_SECRETS"
echo ""
if [[ "$INCLUDE_SECRETS" == "true" ]]; then
echo "########################################################"
echo "# ATTENZIONE: i Secret verranno esportati in CHIARO #"
echo "# (base64, NON cifrato). Non committarli in Git così #"
echo "# come sono. Usa Sealed Secrets, SOPS o External Secrets #"
echo "# Operator prima di aggiungerli al repo. #"
echo "########################################################"
echo ""
fi
# ---------------------------------------------------------------------
# Determina i kind namespaced da processare
# ---------------------------------------------------------------------
get_kinds() {
if [[ -n "$CUSTOM_KINDS" ]]; then
echo "$CUSTOM_KINDS" | tr ',' '\n'
return
fi
# Discovery automatica: tutti i kind namespaced supportati dal cluster
# (copre anche le CRD installate, es. httproute, podmonitor, cluster
# CNPG, ecc.), escludendo quelli in EXCLUDED_KINDS.
kubectl api-resources --namespaced=true --verbs=list -o name 2>/dev/null \
| cut -d. -f1 \
| sort -u \
| while read -r kind; do
skip="false"
for excl in $EXCLUDED_KINDS; do
[[ "$kind" == "$excl" ]] && skip="true" && break
done
[[ "$skip" == "false" ]] && echo "$kind"
done
}
KINDS=$(get_kinds)
# ---------------------------------------------------------------------
# Estrazione
# ---------------------------------------------------------------------
TOTAL_EXPORTED=0
TOTAL_EMPTY=0
TOTAL_ERRORS=0
for ns in "${NAMESPACES[@]}"; do
echo "=== Namespace: $ns ==="
if ! kubectl get namespace "$ns" >/dev/null 2>&1; then
echo " ! Namespace '$ns' non trovato, salto." >&2
continue
fi
ns_dir="${OUTPUT_DIR}/${ns}"
mkdir -p "$ns_dir"
while IFS= read -r kind; do
[[ -z "$kind" ]] && continue
# Salta i secret a meno che non richiesti esplicitamente
if [[ "$kind" == "secrets" || "$kind" == "secret" ]] && [[ "$INCLUDE_SECRETS" != "true" ]]; then
continue
fi
# Conta quante risorse di questo kind esistono nel namespace, per
# evitare di scrivere file vuoti/inutili
count=$(kubectl get "$kind" -n "$ns" --no-headers 2>/dev/null | wc -l | tr -d ' ')
if [[ "$count" -eq 0 ]]; then
TOTAL_EMPTY=$((TOTAL_EMPTY + 1))
continue
fi
out_file="${ns_dir}/${kind}.yaml"
if kubectl get "$kind" -n "$ns" -o yaml 2>/dev/null | kubectl neat > "$out_file" 2>/dev/null; then
# kubectl neat su una List vuota/malformata può comunque produrre
# un file quasi-vuoto: verifichiamo che contenga davvero "kind:"
if grep -q "^kind:" "$out_file" 2>/dev/null || grep -q "^items:" "$out_file" 2>/dev/null; then
echo " + ${kind} (${count})"
TOTAL_EXPORTED=$((TOTAL_EXPORTED + 1))
else
rm -f "$out_file"
fi
else
echo " ! Errore esportando ${kind}" >&2
rm -f "$out_file"
TOTAL_ERRORS=$((TOTAL_ERRORS + 1))
fi
done <<< "$KINDS"
echo ""
done
echo "=== Riepilogo ==="
echo "Risorse esportate: $TOTAL_EXPORTED"
echo "Kind vuoti/saltati: $TOTAL_EMPTY"
echo "Errori: $TOTAL_ERRORS"
echo ""
echo "Output in: $OUTPUT_DIR"
echo ""
echo "Prossimi passi consigliati:"
echo " 1. Rivedi manualmente ogni file: alcuni campi (es. clusterIP,"
echo " nodePort, annotazioni iniettate da controller/webhook) vanno"
echo " rimossi a mano perché non fanno parte del 'desired state'."
echo " 2. Se hai esportato Secret, cifrali (Sealed Secrets / SOPS) prima"
echo " di committarli."
echo " 3. Riorganizza i file nella struttura del repo GitOps"
echo " (infrastructure/ vs apps/, vedi README del repo)."
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env bash
set -euo pipefail
OUTPUT_DIR="./generated-helmops"
API_VERSION="fleet.cattle.io/v1alpha1"
RESOURCE_KIND="HelmOp"
VALUES_ARGS=()
WORKSPACE_NAMESPACE="fleet-local"
usage() {
cat <<'EOF'
Usage:
./generate-fleet-helmops-from-helm.sh [options]
Description:
Legge le release presenti con `helm list -A`, recupera metadata e values
per ogni release e genera un file YAML per release in formato HelmOp-style.
Options:
-o, --output-dir <dir> Directory di output (default: ./generated-helmops)
-w, --workspace-namespace <ns>
Namespace dove creare la risorsa HelmOp
(default: fleet-local)
--api-version <value> apiVersion del manifest generato
(default: fleet.cattle.io/v1alpha1)
--kind <value> kind del manifest generato (default: HelmOp)
-a, --all-values Usa `helm get values -a -o yaml`
-h, --help Mostra questo help
Examples:
./generate-fleet-helmops-from-helm.sh
./generate-fleet-helmops-from-helm.sh -o ./clusters/idc/imported
./generate-fleet-helmops-from-helm.sh -w fleet-default
./generate-fleet-helmops-from-helm.sh --kind HelmChart --api-version helm.cattle.io/v1
EOF
}
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Errore: comando richiesto non trovato: $cmd" >&2
exit 1
fi
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
safe_file_name() {
printf '%s' "$1" | tr '/\\:' '---'
}
extract_yaml_scalar() {
local key="$1"
local content="$2"
printf '%s\n' "$content" \
| sed -n "s/^[[:space:]]*${key}:[[:space:]]*//p" \
| head -n 1 \
| sed 's/^"//; s/"$//; s/^\x27//; s/\x27$//'
}
extract_first_source() {
local content="$1"
awk '
/^sources:/ { in_sources=1; next }
in_sources && /^[^[:space:]-]/ { exit }
in_sources && /^[[:space:]]*-[[:space:]]*/ {
sub(/^[[:space:]]*-[[:space:]]*/, "")
print
exit
}
' <<< "$content"
}
extract_chart_name_from_ref() {
local chart_ref="$1"
if [[ "$chart_ref" =~ ^(.+)-([0-9][A-Za-z0-9.+_-]*)$ ]]; then
printf '%s' "${BASH_REMATCH[1]}"
else
printf '%s' "$chart_ref"
fi
}
extract_chart_version_from_ref() {
local chart_ref="$1"
if [[ "$chart_ref" =~ ^(.+)-([0-9][A-Za-z0-9.+_-]*)$ ]]; then
printf '%s' "${BASH_REMATCH[2]}"
fi
}
find_repo_url_from_local_cache() {
local chart_name="$1"
local chart_version="$2"
local search_json=""
local repo_alias=""
[[ -z "$chart_name" ]] && return 0
search_json="$(helm search repo "$chart_name" --versions -o json 2>/dev/null || true)"
[[ -z "$search_json" || "$search_json" == "[]" ]] && return 0
if [[ -n "$chart_version" ]]; then
repo_alias="$(jq -r --arg chart "$chart_name" --arg version "$chart_version" '
map(select((.name | split("/") | last) == $chart and .version == $version))
| .[0].name // empty
' <<< "$search_json")"
fi
if [[ -z "$repo_alias" ]]; then
repo_alias="$(jq -r --arg chart "$chart_name" '
map(select((.name | split("/") | last) == $chart))
| .[0].name // empty
' <<< "$search_json")"
fi
[[ -z "$repo_alias" ]] && return 0
repo_alias="${repo_alias%%/*}"
helm repo list -o json 2>/dev/null \
| jq -r --arg alias "$repo_alias" 'map(select(.name == $alias)) | .[0].url // empty'
}
indent_file() {
local file_path="$1"
sed 's/^/ /' "$file_path"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output-dir)
OUTPUT_DIR="$2"
shift 2
;;
-w|--workspace-namespace)
WORKSPACE_NAMESPACE="$2"
shift 2
;;
--api-version)
API_VERSION="$2"
shift 2
;;
--kind)
RESOURCE_KIND="$2"
shift 2
;;
-a|--all-values)
VALUES_ARGS=(-a)
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Opzione sconosciuta: $1" >&2
usage >&2
exit 1
;;
esac
done
require_cmd helm
require_cmd jq
mkdir -p "$OUTPUT_DIR"
releases_json="$(helm list -A -o json)"
release_count="$(jq 'length' <<< "$releases_json")"
if [[ "$release_count" -eq 0 ]]; then
echo "Nessuna release Helm trovata." >&2
exit 0
fi
echo "==> Release trovate: $release_count"
echo "==> Directory output: $OUTPUT_DIR"
generated_count=0
warning_count=0
while IFS= read -r release; do
name="$(jq -r '.name' <<< "$release")"
namespace="$(jq -r '.namespace' <<< "$release")"
chart_ref="$(jq -r '.chart // ""' <<< "$release")"
metadata_yaml="$(helm get metadata "$name" -n "$namespace" -o yaml 2>/dev/null || true)"
if [[ -z "$metadata_yaml" ]]; then
echo "! Impossibile leggere metadata per ${namespace}/${name}, salto." >&2
warning_count=$((warning_count + 1))
continue
fi
repo_url="$(trim "$(extract_yaml_scalar repo "$metadata_yaml")")"
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(extract_yaml_scalar repository "$metadata_yaml")")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(extract_yaml_scalar repoURL "$metadata_yaml")")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(extract_first_source "$metadata_yaml")")"
fi
chart_name="$(trim "$(extract_yaml_scalar chart "$metadata_yaml")")"
chart_version="$(trim "$(extract_yaml_scalar version "$metadata_yaml")")"
if [[ -z "$chart_name" ]]; then
chart_name="$(extract_chart_name_from_ref "$chart_ref")"
fi
if [[ -z "$chart_version" ]]; then
chart_version="$(extract_chart_version_from_ref "$chart_ref")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="$(trim "$(find_repo_url_from_local_cache "$chart_name" "$chart_version")")"
fi
if [[ -z "$repo_url" ]]; then
repo_url="REPO_URL_NOT_FOUND"
echo "! Repo URL non trovato in metadata o cache locale per ${namespace}/${name}" >&2
warning_count=$((warning_count + 1))
fi
if [[ -z "$chart_name" ]]; then
chart_name="CHART_NAME_NOT_FOUND"
echo "! Chart name non trovato per ${namespace}/${name}" >&2
warning_count=$((warning_count + 1))
fi
values_file="$(mktemp)"
if ! helm get values "$name" -n "$namespace" "${VALUES_ARGS[@]}" -o yaml > "$values_file" 2>/dev/null; then
printf '{}\n' > "$values_file"
echo "! Values non disponibili per ${namespace}/${name}, uso {}" >&2
warning_count=$((warning_count + 1))
fi
if [[ ! -s "$values_file" ]]; then
printf '{}\n' > "$values_file"
fi
values_compact="$(tr -d '[:space:]' < "$values_file")"
ns_dir="${OUTPUT_DIR}/${namespace}"
mkdir -p "$ns_dir"
safe_name="$(safe_file_name "$name")"
out_file="${ns_dir}/${safe_name}-helmop.yaml"
cat > "$out_file" <<EOF
apiVersion: ${API_VERSION}
kind: ${RESOURCE_KIND}
metadata:
name: ${safe_name}
namespace: ${WORKSPACE_NAMESPACE}
spec:
namespace: ${namespace}
helm:
releaseName: ${name}
repo: ${repo_url}
chart: ${chart_name}
EOF
if [[ -n "$chart_version" ]]; then
cat >> "$out_file" <<EOF
version: ${chart_version}
EOF
fi
if [[ "$values_compact" == "{}" ]]; then
cat >> "$out_file" <<EOF
values: {}
EOF
else
cat >> "$out_file" <<EOF
values:
$(indent_file "$values_file")
EOF
fi
rm -f "$values_file"
echo "+ Generato ${out_file} da ${namespace}/${name}"
generated_count=$((generated_count + 1))
done < <(jq -c '.[]' <<< "$releases_json")
echo ""
echo "=== Riepilogo ==="
echo "File generati: $generated_count"
echo "Warning: $warning_count"
echo "Output: $OUTPUT_DIR"
echo "Workspace namespace: $WORKSPACE_NAMESPACE"
echo ""
echo "Nota: il repo URL viene letto da 'helm get metadata' e, se assente,"
echo " viene cercato nella cache locale di 'helm repo list/search repo'."
echo "Se vedi REPO_URL_NOT_FOUND, completa il campo repo manualmente."