1be7cf2b7f
CI / test (push) Successful in 1m41s
Implements the narrowed Stage 1 scope from ROADMAP.md, against the bootstrap-flow fix from DESIGN.md §4.1/§6 (the earlier self-registration flow couldn't work unauthenticated against terdut-server's real AuthMiddleware -- see that commit for the full trace). - api/v1alpha1: TerdutServer with spec.endpoint + spec.credentialsSecretRef + spec.allowedTeams (image/replicas/networking/database deferred to Stage 5, per DESIGN.md's own narrowing). SecretKeyRef has no namespace field -- always the operator's own, by construction. - internal/controller: TerdutServerReconciler implements exactly the bring-your-own path -- adopt spec.credentialsSecretRef if the Secret exists and has data under the given key, probe GET /api/version as a reachability check, set Ready/Bootstrapped conditions accordingly. Self-registration (the /api/bootstrap race) is not implemented; unset spec.credentialsSecretRef reports Ready: False, reason: CredentialsSecretRefRequired, not an attempt at a flow that would fail unauthenticated anyway. No finalizer: this stage creates nothing server-side and adopts rather than generates its Secret, so there's nothing to clean up on delete yet. - internal/tdclient: minimal terdut-server API client (Version only, the one call this stage needs), styled after terdut-tui's own internal/api/client.go per terdut/CLAUDE.md's mirroring convention. - Tests: envtest suite covering all four not-ready paths plus the happy path (fake terdut-server via httptest.Server, per DESIGN.md §11), and a focused unit suite for tdclient. 75.6%/82.4% coverage. - Event recording uses the new events.k8s.io/v1 recorder API (mgr.GetEventRecorder), not the deprecated GetEventRecorderFor -- caught by golangci-lint's staticcheck before it shipped. Verified locally: make fmt lint test build all clean, 0 lint issues, all specs pass.
217 lines
8.0 KiB
Go
217 lines
8.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1"
|
|
)
|
|
|
|
const (
|
|
// unusedEndpoint is a syntactically valid URL no test here ever expects
|
|
// to actually be dialed (the cases using it fail before reaching the
|
|
// server-reachability check).
|
|
unusedEndpoint = "http://unused.invalid"
|
|
// tokenKey is the data key every test's credentials Secret uses.
|
|
tokenKey = "token"
|
|
)
|
|
|
|
// fakeTerdutServer is an httptest.Server standing in for terdut-server's
|
|
// GET /api/version, per DESIGN.md §11 ("terdut-server's REST API is faked
|
|
// with a small httptest.Server per controller test ... no real Postgres or
|
|
// real terdut-server binary needed for controller unit tests").
|
|
func fakeTerdutServer(version string) *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/version" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
fmt.Fprintf(w, `{"version":%q}`, version) //nolint:errcheck
|
|
}))
|
|
}
|
|
|
|
var _ = Describe("TerdutServer Controller", func() {
|
|
const operatorNamespace = "default"
|
|
|
|
var (
|
|
reconciler *TerdutServerReconciler
|
|
name string
|
|
objKey types.NamespacedName
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
reconciler = &TerdutServerReconciler{
|
|
Client: k8sClient,
|
|
Scheme: k8sClient.Scheme(),
|
|
OperatorNamespace: operatorNamespace,
|
|
}
|
|
name = fmt.Sprintf("test-server-%d-%d", GinkgoRandomSeed(), GinkgoParallelProcess())
|
|
objKey = types.NamespacedName{Name: name, Namespace: operatorNamespace}
|
|
})
|
|
|
|
AfterEach(func(ctx SpecContext) {
|
|
srv := &terdutv1alpha1.TerdutServer{}
|
|
if err := k8sClient.Get(ctx, objKey, srv); err == nil {
|
|
Expect(k8sClient.Delete(ctx, srv)).To(Succeed())
|
|
}
|
|
})
|
|
|
|
createServer := func(ctx context.Context, spec terdutv1alpha1.TerdutServerSpec) {
|
|
srv := &terdutv1alpha1.TerdutServer{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: operatorNamespace},
|
|
Spec: spec,
|
|
}
|
|
Expect(k8sClient.Create(ctx, srv)).To(Succeed())
|
|
}
|
|
|
|
reconcileOnce := func(ctx context.Context) {
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objKey})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
|
|
readyCondition := func(ctx context.Context) metav1.Condition {
|
|
srv := &terdutv1alpha1.TerdutServer{}
|
|
Expect(k8sClient.Get(ctx, objKey, srv)).To(Succeed())
|
|
c := meta.FindStatusCondition(srv.Status.Conditions, terdutv1alpha1.ConditionReady)
|
|
Expect(c).NotTo(BeNil(), "Ready condition should always be set after a reconcile")
|
|
return *c
|
|
}
|
|
|
|
When("spec.credentialsSecretRef is unset", func() {
|
|
It("reports Ready: False, reason CredentialsSecretRefRequired, and makes no API call", func(ctx SpecContext) {
|
|
createServer(ctx, terdutv1alpha1.TerdutServerSpec{Endpoint: unusedEndpoint})
|
|
reconcileOnce(ctx)
|
|
|
|
cond := readyCondition(ctx)
|
|
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
|
|
Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonCredentialsSecretRefRequired))
|
|
})
|
|
})
|
|
|
|
When("the referenced Secret does not exist", func() {
|
|
It("reports Ready: False, reason CredentialsSecretNotFound", func(ctx SpecContext) {
|
|
createServer(ctx, terdutv1alpha1.TerdutServerSpec{
|
|
Endpoint: unusedEndpoint,
|
|
CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: "does-not-exist", Key: tokenKey},
|
|
})
|
|
reconcileOnce(ctx)
|
|
|
|
cond := readyCondition(ctx)
|
|
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
|
|
Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonCredentialsSecretNotFound))
|
|
})
|
|
})
|
|
|
|
When("the referenced Secret exists but has no data under the given key", func() {
|
|
It("reports Ready: False, reason CredentialsSecretInvalid", func(ctx SpecContext) {
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name + "-creds", Namespace: operatorNamespace},
|
|
Data: map[string][]byte{"wrong-key": []byte("tdsa_something")},
|
|
}
|
|
Expect(k8sClient.Create(ctx, secret)).To(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, secret) }()
|
|
|
|
createServer(ctx, terdutv1alpha1.TerdutServerSpec{
|
|
Endpoint: unusedEndpoint,
|
|
CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: secret.Name, Key: tokenKey},
|
|
})
|
|
reconcileOnce(ctx)
|
|
|
|
cond := readyCondition(ctx)
|
|
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
|
|
Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonCredentialsSecretInvalid))
|
|
})
|
|
})
|
|
|
|
When("the Secret is valid but the server is unreachable", func() {
|
|
It("reports Ready: False, reason ServerUnreachable", func(ctx SpecContext) {
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name + "-creds", Namespace: operatorNamespace},
|
|
Data: map[string][]byte{tokenKey: []byte("tdsa_something")},
|
|
}
|
|
Expect(k8sClient.Create(ctx, secret)).To(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, secret) }()
|
|
|
|
createServer(ctx, terdutv1alpha1.TerdutServerSpec{
|
|
// Port 1 is never listening (closed cleanly and immediately,
|
|
// unlike an unroutable address which would hang on a
|
|
// connect timeout) -- keeps the test fast.
|
|
Endpoint: "http://127.0.0.1:1",
|
|
CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: secret.Name, Key: tokenKey},
|
|
})
|
|
reconcileOnce(ctx)
|
|
|
|
cond := readyCondition(ctx)
|
|
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
|
|
Expect(cond.Reason).To(Equal(terdutv1alpha1.ReasonServerUnreachable))
|
|
})
|
|
})
|
|
|
|
When("the Secret is valid and the server answers /api/version", func() {
|
|
It("reports Ready: True, Bootstrapped: True, and mirrors credentialsSecretRef into status", func(ctx SpecContext) {
|
|
fake := fakeTerdutServer("v0.20.0")
|
|
DeferCleanup(fake.Close)
|
|
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name + "-creds", Namespace: operatorNamespace},
|
|
Data: map[string][]byte{tokenKey: []byte("tdsa_something")},
|
|
}
|
|
Expect(k8sClient.Create(ctx, secret)).To(Succeed())
|
|
defer func() { _ = k8sClient.Delete(ctx, secret) }()
|
|
|
|
createServer(ctx, terdutv1alpha1.TerdutServerSpec{
|
|
Endpoint: fake.URL,
|
|
CredentialsSecretRef: &terdutv1alpha1.SecretKeyRef{Name: secret.Name, Key: tokenKey},
|
|
})
|
|
reconcileOnce(ctx)
|
|
|
|
srv := &terdutv1alpha1.TerdutServer{}
|
|
Expect(k8sClient.Get(ctx, objKey, srv)).To(Succeed())
|
|
|
|
ready := meta.FindStatusCondition(srv.Status.Conditions, terdutv1alpha1.ConditionReady)
|
|
Expect(ready).NotTo(BeNil())
|
|
Expect(ready.Status).To(Equal(metav1.ConditionTrue))
|
|
Expect(ready.Reason).To(Equal(terdutv1alpha1.ReasonAdopted))
|
|
|
|
bootstrapped := meta.FindStatusCondition(srv.Status.Conditions, terdutv1alpha1.ConditionBootstrapped)
|
|
Expect(bootstrapped).NotTo(BeNil())
|
|
Expect(bootstrapped.Status).To(Equal(metav1.ConditionTrue))
|
|
|
|
Expect(srv.Status.CredentialsSecretRef).NotTo(BeNil())
|
|
Expect(srv.Status.CredentialsSecretRef.Name).To(Equal(secret.Name))
|
|
Expect(srv.Status.CredentialsSecretRef.Key).To(Equal(tokenKey))
|
|
Expect(srv.Status.ObservedGeneration).To(Equal(srv.Generation))
|
|
})
|
|
})
|
|
|
|
When("the TerdutServer object no longer exists", func() {
|
|
It("returns no error (deleted between enqueue and reconcile)", func(ctx SpecContext) {
|
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{Name: "never-created", Namespace: operatorNamespace},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("TerdutServerReconciler sanity", func() {
|
|
It("treats a real apierrors.IsNotFound the same as any other caller would", func() {
|
|
// Guards against a refactor accidentally swapping in a different
|
|
// not-found check that stops matching what client.Client actually
|
|
// returns.
|
|
Expect(apierrors.IsNotFound(apierrors.NewNotFound(
|
|
terdutv1alpha1.GroupVersion.WithResource("terdutservers").GroupResource(), "x"))).To(BeTrue())
|
|
})
|
|
})
|