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 :

  1. CRD + types Go

    • api/apps/v1alpha1/<app>_types.go
    • Embedding de lshmeta.Spec et lshmeta.Status
    • Annotations Kubebuilder (+kubebuilder:object:root=true, +kubebuilder:subresource:status, printcolumns)
    • Annotation // +lsh:gen:api pour générer les helpers.
  2. Fichiers générés

    • zz_generated.deepcopy.go (via controller-gen, donc make generate).
    • zz_generated.<app>_lsh.go (via lsh-gen).
  3. Contrôleur

    • internal/controller/apps/<app>_controller*.go
    • Annotation // +lsh:gen:app-operator sur le type Reconciler.
    • Wiring dans cmd/main.go (setupTenantControllers) et app-operator.
  4. App-operator

    • cmd/app-operator/<app>/main.go généré, avec la bonne signature pour appoperator.Run.
  5. Tests envtest

    • internal/controller/apps/suite_test.go
    • Tests par app type *_controller_test.go
    • CRDs présents dans config/crd/bases
    • Binaries envtest disponibles et version Kubernetes alignée avec k8s.io/api. book-v3.book.kubebuilder
  6. 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

  1. 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"`
}
  1. Lancer la génération :
make manifests generate
  1. Re-tester :
make vet

1.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.Run

Cause

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 []byte ou string :
import _ "embed"
 
//go:embed penpot_controller_config.cue
var penpotConfigSchema []byte
  • Pour un embed.FS :
import "embed"
 
//go:embed config/*.cue
var configFS embed.FS

Rappel de la spec

  • La directive //go:embed doit 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 used

Cause

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 fmt ne supprime pas les imports inutilisés.
  • Utiliser goimports ou l’auto-format de l’IDE (avec intégration goimports/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 BinaryAssetsDirectory dans suite_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 path

Puis, 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.out

Exé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/... -v

7. 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 character

Cause

  • Le nom de l’Ingress est construit avec un suffixe vide : name + "-" + suffixtest-resource--.
  • Un label commence par _ ("_test-resource"), souvent à cause d’un préfixe ajouté à la main dans ApplyLabels ou similaire.

Règles de base

  • Nom d’Ingress (et de beaucoup de ressources) doit respecter la forme RFC 1123 :
  • 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.

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/... -v

9.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"