Guide de troubleshooting pour intégrer une nouvelle application dans libre.sh
Objectif
Ce document sert de mémo pour les développeurs qui ajoutent une nouvelle application “opérée” dans libre.sh (nouveau CRD + contrôleur + app-operator + tests envtest), en s’appuyant sur les problèmes réellement rencontrés (Penpot) et leurs résolutions.
Vue d’ensemble du workflow
Pour qu’une nouvelle app fonctionne correctement dans libre.sh, il faut que tout ceci soit en place :
-
CRD + types Go
api/apps/v1alpha1/<app>_types.go- Embedding de
lshmeta.Specetlshmeta.Status - Annotations Kubebuilder (
+kubebuilder:object:root=true,+kubebuilder:subresource:status, printcolumns) - Annotation
// +lsh:gen:apipour générer les helpers.
-
Fichiers générés
zz_generated.deepcopy.go(viacontroller-gen, doncmake generate).zz_generated.<app>_lsh.go(vialsh-gen).
-
Contrôleur
internal/controller/apps/<app>_controller*.go- Annotation
// +lsh:gen:app-operatorsur le typeReconciler. - Wiring dans
cmd/main.go(setupTenantControllers) et app-operator.
-
App-operator
cmd/app-operator/<app>/main.gogénéré, avec la bonne signature pourappoperator.Run.
-
Tests envtest
internal/controller/apps/suite_test.go- Tests par app type
*_controller_test.go - CRDs présents dans
config/crd/bases - Binaries
envtestdisponibles et version Kubernetes alignée aveck8s.io/api. book-v3.book.kubebuilder
-
Objets K8s valides
- Noms d’Ingress, labels, annotations conformes aux contraintes RFC 1123 et aux règles de validation Kubernetes. book-v2.book.kubebuilder
1. Génération de code : DeepCopy & helpers lsh-gen
1.1. Erreurs DeepCopyObject / runtime.Object
Symptôme
cannot use &Penpot{} as runtime.Object value in SchemeBuilder.Register:
*Penpot does not implement runtime.Object (missing method DeepCopyObject)Cause
Les méthodes DeepCopyObject (et les autres DeepCopy*) n’existent pas encore pour le nouveau type. Elles sont générées par controller-gen et stockées dans zz_generated.deepcopy.go.
Fix
- Vérifier les annotations sur le type principal et la liste :
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type Penpot struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PenpotSpec `json:"spec,omitempty"`
Status PenpotStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type PenpotList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Penpot `json:"items"`
}- Lancer la génération :
make manifests generate- Re-tester :
make vet1.2. Erreurs GetConditions / SetConditions / GetSuspend
Symptôme
*apps.Penpot does not implement controller-runtime.Object (missing method GetConditions)
*apps.Penpot does not implement conditions.Setter (missing method GetConditions)Cause
Le fichier zz_generated.penpot_lsh.go (helpers GetSuspend, GetVersion, GetConditions, etc.) n’a pas été généré par lsh-gen. C’est un générateur interne distinct de controller-gen.
Pré-requis code
Dans le fichier *_types.go :
// PenpotStatus defines the observed state of Penpot.
type PenpotStatus struct {
lshmeta.Status `json:",inline"`
}
// +lsh:gen:api
type Penpot struct { ... }Génération via lsh-gen
Deux options :
- A. Appeler
lsh-gen(méthode “propre”, si vous êtes dans le dépôt libre.sh) :
# Depuis la racine du dépôt
go run ./tools/lsh-gen libre.sh/api/apps/v1alpha1 libre.sh/internal/controller/apps- B. Création manuelle (rapide)
Créer api/apps/v1alpha1/zz_generated.penpot_lsh.go en copiant le contenu d’une app voisine (Forgejo, Hedgedoc) et en remplaçant le nom du type :
// Code generated by lsh-gen; DO NOT EDIT.
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (o *Penpot) GetSuspend() bool { return o.Spec.Suspend }
func (o *Penpot) SetSuspend(v bool) { o.Spec.Suspend = v }
func (o *Penpot) GetVersion() string { return o.Status.Version }
func (o *Penpot) SetVersion(v string) { o.Status.Version = v }
func (o *Penpot) GetImage() string { return o.Spec.Image }
func (o *Penpot) GetConditions() []metav1.Condition { return o.Status.Conditions }
func (o *Penpot) SetConditions(c []metav1.Condition) { o.Status.Conditions = c }2. App-operator : cmd/app-operator/<app>/main.go
2.1. Signature incorrecte de la fonction passée à appoperator.Run
Symptôme
vet: cmd/app-operator/penpot/main.go:17:18:
cannot use (func(mgr ctrl.Manager) error literal)
as func(ctrl.Manager, logr.Logger) value in argument to appoperator.RunCause
appoperator.Run est défini comme :
func Run(setupCtrls func(ctrl.Manager, logr.Logger)) { ... }Si le main.go contient une fonction du genre func(mgr ctrl.Manager) error, le type ne matche pas.
Fix
Toujours suivre le pattern d’une app déjà fonctionnelle (Nextcloud, Forgejo) :
func main() {
appoperator.Run(func(mgr ctrl.Manager, setupLog logr.Logger) {
if err := (&apps.PenpotReconciler{
Client: mgr.GetClient(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Penpot")
os.Exit(1)
}
})
}Raccourci
cp cmd/app-operator/nextcloud/main.go cmd/app-operator/penpot/main.go
# puis s/NextcloudReconciler/PenpotReconciler/ et s/"Nextcloud"/"Penpot"/3. //go:embed et import embed
3.1. Erreur go:embed requires import "embed"
Symptôme
go:embed requires import "embed" (or import _ "embed", if package is not used)Cause
Aussitôt qu’un fichier contient une directive //go:embed, le compilateur s’attend à trouver un import embed (ou _ "embed"), même si le code ne référence pas directement les symboles de ce package. pkg.go
Fix
- Pour une variable
[]byteoustring:
import _ "embed"
//go:embed penpot_controller_config.cue
var penpotConfigSchema []byte- Pour un
embed.FS:
import "embed"
//go:embed config/*.cue
var configFS embed.FSRappel de la spec
- La directive
//go:embeddoit précéder immédiatement la déclaration de une seule variable. - Seules des lignes vides et des commentaires
//sont autorisées entre la directive et la variable. godocs
4. Imports Go inutilisés (fmt, etc.)
4.1. import "... " and not used
Symptôme
"fmt" imported and not usedCause
Go refuse de compiler si un import n’est pas utilisé. C’est une vérification de base du compilateur / go vet.
Fix
- Supprimer l’import si vous ne l’utilisez plus.
- Ou utiliser réellement le package (
fmt.Errorf,fmt.Sprintf, etc.).
Attention
go fmtne supprime pas les imports inutilisés.- Utiliser
goimportsou l’auto-format de l’IDE (avec intégrationgoimports/gopls).
5. Tests avec envtest : suite_test.go & KUBEBUILDER_ASSETS
5.1. Binaries envtest et exécution hors make test
envtest démarre un API server + etcd local, en cherchant les binaires dans un répertoire (par défaut /usr/local/kubebuilder/bin ou KUBEBUILDER_ASSETS). pkg.go
Deux modes :
- Via
BinaryAssetsDirectorydanssuite_test.go. - Via la variable d’environnement
KUBEBUILDER_ASSETS(recommandé par Kubebuilder). book-v2.book.kubebuilder
Pattern recommandé Kubebuilder book.kubebuilder
Dans le Makefile :
ENVTEST ?= $(LOCALBIN)/setup-envtest
# Version de controller-runtime → version de setup-envtest
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
# Version de Kubernetes pour envtest, alignée avec k8s.io/api
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
.PHONY: setup-envtest
setup-envtest: envtest
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p pathPuis, dans la target test :
test: manifests generate fmt vet
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \
go test ./... -coverprofile cover.outExécution locale hors make test
export KUBEBUILDER_ASSETS=$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@latest \
use $(go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $3}') \
--bin-dir ./bin -p path)
go test ./internal/controller/apps/... -v --ginkgo.focus "Penpot"Références : doc Kubebuilder sur envtest & setup-envtest. book-v3.book.kubebuilder
6. Ginkgo / go test : filtres et “no tests to run”
6.1. go test -run "Penpot" → no tests to run
Symptôme
testing: warning: no tests to run
PASS
ok ...Cause
Le flag natif -run filtre les fonctions TestXxx du package Go standard. Dans un projet Ginkgo/Kubebuilder, il n’y a souvent qu’une seule fonction :
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}Donc -run "Penpot" ne matche rien. Les vrais tests sont dans les Describe/Context/It.
Fix
Utiliser le filtre Ginkgo :
go test ./internal/controller/apps/... -v --ginkgo.focus "Penpot"Ou lancer l’ensemble du package :
go test ./internal/controller/apps/... -v7. Problèmes fréquents de validation Kubernetes (Ingress, labels)
7.1. Ingress invalide : nom et labels
Symptôme
Ingress.networking.k8s.io "test-resource--" is invalid:
metadata.name: Invalid value: "test-resource--":
a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.',
and must start and end with an alphanumeric character
...
metadata.labels: Invalid value: "_test-resource":
a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.',
and must start and end with an alphanumeric characterCause
- Le nom de l’Ingress est construit avec un suffixe vide :
name + "-" + suffix→test-resource--. - Un label commence par
_("_test-resource"), souvent à cause d’un préfixe ajouté à la main dansApplyLabelsou similaire.
Règles de base
- Nom d’Ingress (et de beaucoup de ressources) doit respecter la forme RFC 1123 :
- commence et finit par
[a-z0-9], - caractères intermédiaires
[a-z0-9.-]. book-v2.book.kubebuilder
- commence et finit par
- Label value :
([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9](pas de début/fin par_ou-).
Fix
- Ne jamais concaténer
"-"avec un suffixe potentiellement vide.- soit pas de suffixe, soit un suffixe constant non vide.
- S’assurer que les composants passés à un helper de labels ne commencent ni par
_ni par-.
Exemple correct pour un Ingress simple :
ObjectMeta: metav1.ObjectMeta{
Name: lshr.GetResourceName(penpot), // "penpot-test"
Namespace: penpot.Namespace,
},8. Tests de contrôleur : pattern minimal
8.1. Pattern type Ginkgo pour un contrôleur d’app
Exemple simplifié :
var _ = Describe("Penpot Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "penpot-test"
ctx := context.Background()
nn := types.NamespacedName{Name: resourceName, Namespace: "default"}
penpot := &appsv1alpha1.Penpot{}
BeforeEach(func() {
By("ensuring Mailbox pre-requisite exists")
// créer un Mailbox CR ou son Secret attendu
By("creating the Penpot CR")
err := k8sClient.Get(ctx, nn, penpot)
if err != nil && errors.IsNotFound(err) {
resource := &appsv1alpha1.Penpot{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
Spec: appsv1alpha1.PenpotSpec{
Host: "penpot.test",
// champs obligatoires d’après le PenpotSpec
},
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
resource := &appsv1alpha1.Penpot{}
err := k8sClient.Get(ctx, nn, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleaning up the Penpot resource")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should reconcile successfully", func() {
By("Running the reconciler once")
r := &PenpotReconciler{Client: k8sClient}
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: nn})
Expect(err).NotTo(HaveOccurred())
// assertions supplémentaires (Ingress, CRs dépendants, Secrets, etc.)
})
})
})Points d’attention
- Les dépendances (Postgres, Redis, Bucket, Mailbox, OIDC) sont souvent créées par le contrôleur, mais leurs Secrets sont supposés prêts au second reconcile. Dans les tests, il faut donc :
- soit simuler les CR dépendants et leurs secrets dans
BeforeEach, - soit ajuster les attentes : premier reconcile = création des CR + Ingress, mais pas encore des Deployments.
- soit simuler les CR dépendants et leurs secrets dans
9. Raccourcis et commandes utiles
9.1. Boucle de dev quotidienne
# Générer CRDs + deepcopies après modif des types
make manifests generate
# Vérifier compilation statique
make vet
# Lancer tous les tests des controllers apps
make test # si configuré avec envtest
# ou
go test ./internal/controller/apps/... -v9.2. Tests ciblés sur une app
export KUBEBUILDER_ASSETS=$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@latest \
use $(go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $3}') \
--bin-dir ./bin -p path)
go test ./internal/controller/apps/... -v --ginkgo.focus "Penpot"