feat: add bare policy binding controller
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
// AI generated tests and not yet reviewed.
|
||||
package policybinding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1alpha1 "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/apis/policybinding/v1alpha1"
|
||||
operatorfake "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/clientset/versioned/fake"
|
||||
operatorinformers "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/informers/externalversions"
|
||||
authentikapi "goauthentik.io/api/v3"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func TestController_syncHandler_create(t *testing.T) {
|
||||
const wantPK = "42"
|
||||
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
policyBindingCreate: func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK})
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, testPolicyBinding(), server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "test-pb"})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
|
||||
got := getPolicyBinding(t, ctrl, "default", "test-pb")
|
||||
if got.Status.PK != wantPK {
|
||||
t.Fatalf("status.pk = %q, want %q", got.Status.PK, wantPK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_ensureFinalizers(t *testing.T) {
|
||||
pb := testPolicyBinding()
|
||||
pb.Status.PK = "42"
|
||||
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, pb, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pb.Namespace, Name: pb.Name})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
|
||||
got := getPolicyBinding(t, ctrl, pb.Namespace, pb.Name)
|
||||
if !slices.Contains(got.Finalizers, DeleteAuthentikPolicyBindingFinalizer) {
|
||||
t.Fatalf("finalizers = %v, want %q", got.Finalizers, DeleteAuthentikPolicyBindingFinalizer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_update(t *testing.T) {
|
||||
pb := testPolicyBinding()
|
||||
pb.Status.PK = "42"
|
||||
pb.Finalizers = []string{DeleteAuthentikPolicyBindingFinalizer}
|
||||
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
policyBindingRetrieve: func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"pk": "42"})
|
||||
},
|
||||
policyBindingPartialUpdate: func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"pk": "42"})
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, pb, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pb.Namespace, Name: pb.Name})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
|
||||
got := getPolicyBinding(t, ctrl, pb.Namespace, pb.Name)
|
||||
if got.Status.PK != "42" {
|
||||
t.Fatalf("status.pk = %q, want 42", got.Status.PK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_update_policyBindingNotFound(t *testing.T) {
|
||||
pb := testPolicyBinding()
|
||||
pb.Status.PK = "42"
|
||||
pb.Finalizers = []string{DeleteAuthentikPolicyBindingFinalizer}
|
||||
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
policyBindingRetrieve: func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.NotFound(w, nil)
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, pb, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pb.Namespace, Name: pb.Name})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
|
||||
got := getPolicyBinding(t, ctrl, pb.Namespace, pb.Name)
|
||||
if got.Status.PK != "" {
|
||||
t.Fatalf("status.pk = %q, want empty after policy binding not found", got.Status.PK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_delete(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
pb := testPolicyBinding()
|
||||
pb.Status.PK = "42"
|
||||
pb.DeletionTimestamp = &now
|
||||
pb.Finalizers = []string{DeleteAuthentikPolicyBindingFinalizer}
|
||||
|
||||
var destroyCalled bool
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
policyBindingDestroy: func(w http.ResponseWriter, r *http.Request) {
|
||||
destroyCalled = true
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("destroy method = %s, want DELETE", r.Method)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, pb, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pb.Namespace, Name: pb.Name})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
if !destroyCalled {
|
||||
t.Fatal("expected Authentik destroy call")
|
||||
}
|
||||
|
||||
got := getPolicyBinding(t, ctrl, pb.Namespace, pb.Name)
|
||||
if slices.Contains(got.Finalizers, DeleteAuthentikPolicyBindingFinalizer) {
|
||||
t.Fatalf("finalizers = %v, want finalizer removed", got.Finalizers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_delete_policyBindingAlreadyGone(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
pb := testPolicyBinding()
|
||||
pb.Status.PK = "42"
|
||||
pb.DeletionTimestamp = &now
|
||||
pb.Finalizers = []string{DeleteAuthentikPolicyBindingFinalizer}
|
||||
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
policyBindingDestroy: func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.NotFound(w, nil)
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, pb, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pb.Namespace, Name: pb.Name})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
|
||||
got := getPolicyBinding(t, ctrl, pb.Namespace, pb.Name)
|
||||
if slices.Contains(got.Finalizers, DeleteAuthentikPolicyBindingFinalizer) {
|
||||
t.Fatalf("finalizers = %v, want finalizer removed after 404", got.Finalizers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_notFound(t *testing.T) {
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, nil, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "missing"})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v, want nil for missing object", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_enqueuePolicyBinding(t *testing.T) {
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, _, cancel := newTestController(t, testPolicyBinding(), server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
ctrl.enqueuePolicyBinding(testPolicyBinding())
|
||||
|
||||
if ctrl.workqueue.Len() != 1 {
|
||||
t.Fatalf("workqueue length = %d, want 1", ctrl.workqueue.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// --- test helpers ---
|
||||
|
||||
func testPolicyBinding() *v1alpha1.PolicyBinding {
|
||||
return &v1alpha1.PolicyBinding{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "PolicyBinding",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pb",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.PolicyBindingSpec{
|
||||
Group: "14ab813f-a7f9-481b-9b08-781953ae9ebf",
|
||||
Target: "8dd85627-9c48-49c2-8afc-d73dd122ffc2",
|
||||
Order: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTestController(t *testing.T, pb *v1alpha1.PolicyBinding, authentikURL string) (*Controller, context.Context, context.CancelFunc) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctrl, _, stop := newTestControllerWithContext(t, ctx, pb, authentikURL)
|
||||
return ctrl, ctx, func() {
|
||||
cancel()
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
func newTestControllerWithContext(t *testing.T, ctx context.Context, pb *v1alpha1.PolicyBinding, authentikURL string) (*Controller, context.Context, func()) {
|
||||
t.Helper()
|
||||
|
||||
authentikClient := newAuthentikAPIClientForTest(t, authentikURL)
|
||||
|
||||
var objects []runtime.Object
|
||||
if pb != nil {
|
||||
objects = append(objects, pb)
|
||||
}
|
||||
policyBindingClient := operatorfake.NewSimpleClientset(objects...)
|
||||
|
||||
informerFactory := operatorinformers.NewSharedInformerFactory(policyBindingClient, 0)
|
||||
policyBindingInformer := informerFactory.PolicyBinding().V1alpha1().PolicyBindings()
|
||||
|
||||
ctrl := NewController(ctx, fake.NewClientset(), policyBindingClient, authentikClient, policyBindingInformer)
|
||||
|
||||
informerFactory.Start(ctx.Done())
|
||||
for informerType, synced := range informerFactory.WaitForCacheSync(ctx.Done()) {
|
||||
if !synced {
|
||||
t.Fatalf("informer %v failed to sync", informerType)
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl, ctx, func() {}
|
||||
}
|
||||
|
||||
func newAuthentikAPIClientForTest(t *testing.T, serverURL string) *authentikapi.APIClient {
|
||||
t.Helper()
|
||||
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse server URL: %v", err)
|
||||
}
|
||||
|
||||
cfg := authentikapi.NewConfiguration()
|
||||
cfg.Scheme = u.Scheme
|
||||
cfg.Host = u.Host
|
||||
|
||||
return authentikapi.NewAPIClient(cfg)
|
||||
}
|
||||
|
||||
type authentikTestHandlers struct {
|
||||
policyBindingCreate http.HandlerFunc
|
||||
policyBindingRetrieve http.HandlerFunc
|
||||
policyBindingPartialUpdate http.HandlerFunc
|
||||
policyBindingDestroy http.HandlerFunc
|
||||
}
|
||||
|
||||
func newAuthentikTestServer(t *testing.T, handlers authentikTestHandlers) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
switch {
|
||||
case path == "/api/v3/policies/bindings/" && r.Method == http.MethodPost:
|
||||
if handlers.policyBindingCreate != nil {
|
||||
handlers.policyBindingCreate(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
|
||||
case strings.HasPrefix(path, "/api/v3/policies/bindings/") && strings.HasSuffix(path, "/"):
|
||||
idPath := strings.TrimPrefix(path, "/api/v3/policies/bindings/")
|
||||
if idPath == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if handlers.policyBindingRetrieve != nil {
|
||||
handlers.policyBindingRetrieve(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
case http.MethodPatch:
|
||||
if handlers.policyBindingPartialUpdate != nil {
|
||||
handlers.policyBindingPartialUpdate(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
case http.MethodDelete:
|
||||
if handlers.policyBindingDestroy != nil {
|
||||
handlers.policyBindingDestroy(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
default:
|
||||
http.Error(w, "unexpected method on policy binding instance", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, status int, body any) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(body); err != nil {
|
||||
t.Fatalf("write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getPolicyBinding(t *testing.T, ctrl *Controller, namespace, name string) *v1alpha1.PolicyBinding {
|
||||
t.Helper()
|
||||
|
||||
got, err := ctrl.policyBindingClientset.PolicyBindingV1alpha1().PolicyBindings(namespace).Get(
|
||||
context.Background(), name, metav1.GetOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("get PolicyBinding: %v", err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
Reference in New Issue
Block a user