feat: allow proxy provider to reference an outpost to be added to
CI / image check (pull_request) Failing after 6s
CI / install-dependencies (pull_request) Successful in 7m48s
CI / build check (pull_request) Successful in 27s
CI / test (pull_request) Successful in 22s
CI / check lint (pull_request) Successful in 3m8s
CI / check format (pull_request) Successful in 7m29s
CI / image check (pull_request) Failing after 6s
CI / install-dependencies (pull_request) Successful in 7m48s
CI / build check (pull_request) Successful in 27s
CI / test (pull_request) Successful in 22s
CI / check lint (pull_request) Successful in 3m8s
CI / check format (pull_request) Successful in 7m29s
This commit is contained in:
@@ -166,6 +166,12 @@ func (c *ProxyProviderController) reconcileDelete(ctx context.Context, pp *v1alp
|
||||
return fmt.Errorf("error parsing PK: %v", err)
|
||||
}
|
||||
|
||||
err = c.reconcileOutpost(ctx, pp.Spec.Outpost, int32(pk), ReconcileOutpostModeRemove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when calling `reconcileOutpost`: %w", err)
|
||||
}
|
||||
|
||||
// Delete ProxyProvider
|
||||
r, err := c.authentik.ProvidersApi.ProvidersProxyDestroy(ctx, int32(pk)).Execute()
|
||||
if err != nil {
|
||||
// This handles an edge-case, where when the ProxyProvider on Authentik has already been deleted, but the finalizer is still present. We just remove the finalizer and return.
|
||||
@@ -225,6 +231,11 @@ func (c *ProxyProviderController) reconcileCreate(ctx context.Context, pp *v1alp
|
||||
return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyCreate`: %w with response %v", err, r)
|
||||
}
|
||||
|
||||
err = c.reconcileOutpost(ctx, pp.Spec.Outpost, resp.Pk, ReconcileOutpostModeAdd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when calling `reconcileOutpost`: %w", err)
|
||||
}
|
||||
|
||||
pp.Status.PK = strconv.Itoa(int(resp.Pk))
|
||||
return c.updateProxyProviderStatus(ctx, pp)
|
||||
}
|
||||
@@ -244,3 +255,51 @@ func (c *ProxyProviderController) updateProxyProvider(ctx context.Context, pp *v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReconcileOutpostMode string
|
||||
|
||||
const (
|
||||
ReconcileOutpostModeAdd ReconcileOutpostMode = "add"
|
||||
ReconcileOutpostModeRemove ReconcileOutpostMode = "remove"
|
||||
)
|
||||
|
||||
func (c *ProxyProviderController) reconcileOutpost(ctx context.Context, outpostId string, providerPk int32, mode ReconcileOutpostMode) error {
|
||||
logger := klog.LoggerWithValues(klog.FromContext(ctx), "outpostId", outpostId, "providerPk", providerPk, "mode", mode)
|
||||
|
||||
outpost, r, err := c.authentik.OutpostsApi.OutpostsInstancesRetrieve(ctx, outpostId).Execute()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when calling `OutpostsAPI.OutpostsInstancesRetrieve`: %w with response %v", err, r)
|
||||
}
|
||||
updated := false
|
||||
|
||||
switch mode {
|
||||
case ReconcileOutpostModeAdd:
|
||||
if !slices.Contains(outpost.Providers, providerPk) {
|
||||
outpost.Providers = append(outpost.Providers, providerPk)
|
||||
updated = true
|
||||
} else {
|
||||
logger.V(4).Info("Provider already in outpost")
|
||||
}
|
||||
case ReconcileOutpostModeRemove:
|
||||
if slices.Contains(outpost.Providers, providerPk) {
|
||||
outpost.Providers = slices.Delete(outpost.Providers, slices.Index(outpost.Providers, providerPk), 1)
|
||||
updated = true
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
|
||||
outpostPartialUpdateRequest := &authentikapi.PatchedOutpostRequest{
|
||||
Providers: outpost.Providers,
|
||||
}
|
||||
_, r, err = c.authentik.OutpostsApi.OutpostsInstancesPartialUpdate(ctx, outpostId).PatchedOutpostRequest(*outpostPartialUpdateRequest).Execute()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when calling `OutpostsAPI.OutpostsInstancesPartialUpdate`: %w with response %v", err, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,13 +21,30 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
const testOutpostID = "550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
func TestController_syncHandler_create(t *testing.T) {
|
||||
const wantPK = 42
|
||||
|
||||
var outpostPartialUpdateCalled bool
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
proxyCreate: func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK})
|
||||
},
|
||||
outpostRetrieve: outpostRetrieveHandler(t, nil),
|
||||
outpostPartialUpdate: func(w http.ResponseWriter, r *http.Request) {
|
||||
outpostPartialUpdateCalled = true
|
||||
var body struct {
|
||||
Providers []int32 `json:"providers"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode outpost patch body: %v", err)
|
||||
}
|
||||
if !slices.Contains(body.Providers, wantPK) {
|
||||
t.Fatalf("patched providers = %v, want to contain %d", body.Providers, wantPK)
|
||||
}
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"pk": testOutpostID, "providers": body.Providers})
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
@@ -38,6 +55,41 @@ func TestController_syncHandler_create(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
if !outpostPartialUpdateCalled {
|
||||
t.Fatal("expected Authentik outpost partial update call")
|
||||
}
|
||||
|
||||
got := getProxyProvider(t, ctrl, "default", "test-pp")
|
||||
if got.Status.PK != "42" {
|
||||
t.Fatalf("status.pk = %q, want 42", got.Status.PK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_create_providerAlreadyInOutpost(t *testing.T) {
|
||||
const wantPK = 42
|
||||
|
||||
var outpostPartialUpdateCalled bool
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
proxyCreate: func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK})
|
||||
},
|
||||
outpostRetrieve: outpostRetrieveHandler(t, []int32{wantPK}),
|
||||
outpostPartialUpdate: func(w http.ResponseWriter, _ *http.Request) {
|
||||
outpostPartialUpdateCalled = true
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, testProxyProvider(), server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "test-pp"})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
if outpostPartialUpdateCalled {
|
||||
t.Fatal("did not expect Authentik outpost partial update when provider is already present")
|
||||
}
|
||||
|
||||
got := getProxyProvider(t, ctrl, "default", "test-pp")
|
||||
if got.Status.PK != "42" {
|
||||
@@ -122,14 +174,29 @@ func TestController_syncHandler_update_providerNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestController_syncHandler_delete(t *testing.T) {
|
||||
const wantPK int32 = 42
|
||||
now := metav1.Now()
|
||||
pp := testProxyProvider()
|
||||
pp.Status.PK = "42"
|
||||
pp.DeletionTimestamp = &now
|
||||
pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer}
|
||||
|
||||
var destroyCalled bool
|
||||
var outpostPartialUpdateCalled, destroyCalled bool
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
outpostRetrieve: outpostRetrieveHandler(t, []int32{wantPK}),
|
||||
outpostPartialUpdate: func(w http.ResponseWriter, r *http.Request) {
|
||||
outpostPartialUpdateCalled = true
|
||||
var body struct {
|
||||
Providers []int32 `json:"providers"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode outpost patch body: %v", err)
|
||||
}
|
||||
if slices.Contains(body.Providers, wantPK) {
|
||||
t.Fatalf("patched providers = %v, want provider %d removed", body.Providers, wantPK)
|
||||
}
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"pk": testOutpostID, "providers": body.Providers})
|
||||
},
|
||||
proxyDestroy: func(w http.ResponseWriter, r *http.Request) {
|
||||
destroyCalled = true
|
||||
if r.Method != http.MethodDelete {
|
||||
@@ -147,6 +214,49 @@ func TestController_syncHandler_delete(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
if !outpostPartialUpdateCalled {
|
||||
t.Fatal("expected Authentik outpost partial update call")
|
||||
}
|
||||
if !destroyCalled {
|
||||
t.Fatal("expected Authentik destroy call")
|
||||
}
|
||||
|
||||
got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name)
|
||||
if slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) {
|
||||
t.Fatalf("finalizers = %v, want finalizer removed", got.Finalizers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_syncHandler_delete_providerNotInOutpost(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
pp := testProxyProvider()
|
||||
pp.Status.PK = "42"
|
||||
pp.DeletionTimestamp = &now
|
||||
pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer}
|
||||
|
||||
var outpostPartialUpdateCalled, destroyCalled bool
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
outpostRetrieve: outpostRetrieveHandler(t, nil),
|
||||
outpostPartialUpdate: func(w http.ResponseWriter, _ *http.Request) {
|
||||
outpostPartialUpdateCalled = true
|
||||
},
|
||||
proxyDestroy: func(w http.ResponseWriter, _ *http.Request) {
|
||||
destroyCalled = true
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
},
|
||||
})
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
ctrl, ctx, cancel := newTestController(t, pp, server.URL)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name})
|
||||
if err != nil {
|
||||
t.Fatalf("syncHandler() error = %v", err)
|
||||
}
|
||||
if outpostPartialUpdateCalled {
|
||||
t.Fatal("did not expect Authentik outpost partial update when provider is not in outpost")
|
||||
}
|
||||
if !destroyCalled {
|
||||
t.Fatal("expected Authentik destroy call")
|
||||
}
|
||||
@@ -158,6 +268,7 @@ func TestController_syncHandler_delete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestController_syncHandler_delete_providerAlreadyGone(t *testing.T) {
|
||||
const wantPK int32 = 42
|
||||
now := metav1.Now()
|
||||
pp := testProxyProvider()
|
||||
pp.Status.PK = "42"
|
||||
@@ -165,6 +276,16 @@ func TestController_syncHandler_delete_providerAlreadyGone(t *testing.T) {
|
||||
pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer}
|
||||
|
||||
server := newAuthentikTestServer(t, authentikTestHandlers{
|
||||
outpostRetrieve: outpostRetrieveHandler(t, []int32{wantPK}),
|
||||
outpostPartialUpdate: func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Providers []int32 `json:"providers"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode outpost patch body: %v", err)
|
||||
}
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"pk": testOutpostID, "providers": body.Providers})
|
||||
},
|
||||
proxyDestroy: func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.NotFound(w, nil)
|
||||
},
|
||||
@@ -235,6 +356,7 @@ func testProxyProvider() *v1alpha1.ProxyProvider {
|
||||
AuthorizationFlow: "flow-auth",
|
||||
InvalidationFlow: "flow-invalidate",
|
||||
ExternalHost: "https://app.example.com",
|
||||
Outpost: testOutpostID,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -291,10 +413,22 @@ func newAuthentikAPIClientForTest(t *testing.T, serverURL string) *authentikapi.
|
||||
}
|
||||
|
||||
type authentikTestHandlers struct {
|
||||
proxyCreate http.HandlerFunc
|
||||
proxyDestroy http.HandlerFunc
|
||||
proxyPartialUpdate http.HandlerFunc
|
||||
allRetrieve http.HandlerFunc
|
||||
proxyCreate http.HandlerFunc
|
||||
proxyDestroy http.HandlerFunc
|
||||
proxyPartialUpdate http.HandlerFunc
|
||||
allRetrieve http.HandlerFunc
|
||||
outpostRetrieve http.HandlerFunc
|
||||
outpostPartialUpdate http.HandlerFunc
|
||||
}
|
||||
|
||||
func outpostRetrieveHandler(t *testing.T, providers []int32) http.HandlerFunc {
|
||||
t.Helper()
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"pk": testOutpostID,
|
||||
"providers": providers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newAuthentikTestServer(t *testing.T, handlers authentikTestHandlers) *httptest.Server {
|
||||
@@ -341,6 +475,30 @@ func newAuthentikTestServer(t *testing.T, handlers authentikTestHandlers) *httpt
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
|
||||
case strings.HasPrefix(path, "/api/v3/outposts/instances/") && strings.HasSuffix(path, "/"):
|
||||
idPath := strings.TrimPrefix(path, "/api/v3/outposts/instances/")
|
||||
idPath = strings.TrimSuffix(idPath, "/")
|
||||
if idPath == "" || strings.Contains(idPath, "/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if handlers.outpostRetrieve != nil {
|
||||
handlers.outpostRetrieve(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
case http.MethodPatch:
|
||||
if handlers.outpostPartialUpdate != nil {
|
||||
handlers.outpostPartialUpdate(w, r)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
default:
|
||||
http.Error(w, "unexpected method on outpost instance", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user