mvp working creation of proxy provider

This commit is contained in:
2026-05-15 11:09:20 +02:00
parent 93fd4e89d5
commit 90d21f1dd8
11 changed files with 166 additions and 58 deletions
+6 -1
View File
@@ -24,4 +24,9 @@ go.work.sum
# env file
.env
vendor/*
# vendor directory
vendor/
# build artifacts
main
+17
View File
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"args": ["--kubeconfig=/home/tbehrendt/.kube/config"],
"envFile": ".env"
}
]
}
+7 -5
View File
@@ -1,13 +1,15 @@
REPO_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
ifneq (,$(wildcard ./.env))
include .env
export
endif
.PHONY: build run codegen
build:
go build
go build -o main
run:
make build
./main
./main --kubeconfig=/home/tbehrendt/.kube/config
codegen:
$(REPO_ROOT)/scripts/codegen.sh
./scripts/codegen.sh
@@ -8,6 +8,12 @@ spec:
- name: v1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- name: PK
type: string
jsonPath: .status.pk
schema:
openAPIV3Schema:
type: object
@@ -38,4 +44,6 @@ spec:
names:
kind: ProxyProvider
plural: proxyproviders
shortNames:
- pp
scope: Namespaced
-7
View File
@@ -1,7 +0,0 @@
apiVersion: samplecontroller.k8s.io/v1alpha1
kind: Foo
metadata:
name: example-foo
spec:
deploymentName: example-foo
replicas: 1
+11
View File
@@ -0,0 +1,11 @@
# Example ProxyProvider CRD
apiVersion: proxyprovider.t000-n.de/v1
kind: ProxyProvider
metadata:
name: proxy-provider-example
namespace: kube-system
spec:
name: proxy-provider-example
authorization_flow: 16896c6d-b326-42d1-8d3f-93f32921962e
invalidation_flow: 7acac1ef-19e3-4a6f-8d8d-14ca7031d184
external_host: https://example.t00n.de
+64 -4
View File
@@ -19,6 +19,7 @@ package main
import (
"context"
"fmt"
"strconv"
"time"
"golang.org/x/time/rate"
@@ -39,10 +40,12 @@ import (
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
v1 "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/apis/proxyprovider/v1"
clientset "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/clientset/versioned"
operatorscheme "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/clientset/versioned/scheme"
informers "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/informers/externalversions/proxyprovider/v1"
listers "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/listers/proxyprovider/v1"
authentikapi "goauthentik.io/api/v3"
)
const controllerAgentName = "proxy-provider-controller"
@@ -57,7 +60,8 @@ const (
type Controller struct {
kubeclientset kubernetes.Interface
operatorclientset clientset.Interface
proxyProviderClientset clientset.Interface
authentik *authentikapi.APIClient
deploymentsLister appslisters.DeploymentLister
deploymentsSynced cache.InformerSynced
@@ -71,7 +75,8 @@ type Controller struct {
func NewController(
ctx context.Context,
kubeclientset kubernetes.Interface,
operatorclientset clientset.Interface,
proxyProviderClientset clientset.Interface,
authentik *authentikapi.APIClient,
deploymentInformer appsinformers.DeploymentInformer,
proxyInformer informers.ProxyProviderInformer,
) *Controller {
@@ -91,7 +96,8 @@ func NewController(
c := &Controller{
kubeclientset: kubeclientset,
operatorclientset: operatorclientset,
proxyProviderClientset: proxyProviderClientset,
authentik: authentik,
deploymentsLister: deploymentInformer.Lister(),
deploymentsSynced: deploymentInformer.Informer().HasSynced,
proxyLister: proxyInformer.Lister(),
@@ -181,8 +187,55 @@ func (c *Controller) syncHandler(ctx context.Context, objectRef cache.ObjectName
}
return err
}
logger.V(4).Info("sync ProxyProvider", "name", pp.Name)
if pp.Status.PK != "" {
// We retrieve the existing PP from the API by slug.
pk, err := strconv.ParseInt(pp.Status.PK, 10, 32)
if err != nil {
return fmt.Errorf("error parsing PK: %v", err)
}
_, _, err = c.authentik.ProvidersApi.ProvidersAllRetrieve(ctx, int32(pk)).Execute()
if err != nil {
return fmt.Errorf("error retrieving existing ProxyProvider: %v", err)
}
// We update the existing PP with the new spec.
proxyProviderRequest := &authentikapi.ProxyProviderRequest{
Name: pp.Spec.Name,
AuthorizationFlow: pp.Spec.AuthorizationFlow,
InvalidationFlow: pp.Spec.InvalidationFlow,
ExternalHost: pp.Spec.ExternalHost,
Mode: authentikapi.PROXYMODE_FORWARD_SINGLE.Ptr(),
}
resp, r, err := c.authentik.ProvidersApi.ProvidersProxyUpdate(ctx, int32(pk)).ProxyProviderRequest(*proxyProviderRequest).Execute()
if err != nil {
return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyUpdate`: %w with response %v", err, r)
}
pp.Status.PK = strconv.Itoa(int(resp.Pk))
err = c.updateProxyProviderStatus(ctx, pp)
if err != nil {
return fmt.Errorf("error updating ProxyProvider status: %v", err)
}
} else {
proxyProviderRequest := &authentikapi.ProxyProviderRequest{
Name: pp.Spec.Name,
AuthorizationFlow: pp.Spec.AuthorizationFlow,
InvalidationFlow: pp.Spec.InvalidationFlow,
ExternalHost: pp.Spec.ExternalHost,
Mode: authentikapi.PROXYMODE_FORWARD_SINGLE.Ptr(),
}
resp, r, err := c.authentik.ProvidersApi.ProvidersProxyCreate(ctx).ProxyProviderRequest(*proxyProviderRequest).Execute()
if err != nil {
return fmt.Errorf("error when calling `ProvidersAPI.ProvidersProxyCreate`: %w with response %v", err, r)
}
pp.Status.PK = strconv.Itoa(int(resp.Pk))
err = c.updateProxyProviderStatus(ctx, pp)
if err != nil {
return fmt.Errorf("error updating ProxyProvider status: %v", err)
}
}
return nil
}
@@ -211,3 +264,10 @@ func (c *Controller) handleObject(obj interface{}) {
}
}
}
func (c *Controller) updateProxyProviderStatus(ctx context.Context, pp *v1.ProxyProvider) error {
ppCopy := pp.DeepCopy()
ppCopy.Status.PK = pp.Status.PK
_, err := c.proxyProviderClientset.ProxyproviderV1().ProxyProviders(pp.Namespace).UpdateStatus(ctx, ppCopy, metav1.UpdateOptions{FieldManager: FieldManager})
return err
}
+2 -6
View File
@@ -5,14 +5,13 @@ go 1.26.3
godebug default=go1.26
require (
goauthentik.io/api/v3 v3.2026020.16
golang.org/x/time v0.15.0
k8s.io/api v0.0.0-20260509204538-0dfb117cc6ec
k8s.io/apimachinery v0.0.0-20260513183604-f9371b815e42
k8s.io/client-go v0.0.0-20260509205101-ca52b81a2940
k8s.io/code-generator v0.0.0-20260509210052-5595d1310975
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260511211612-da4e56fe5676
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/structured-merge-diff/v6 v6.4.0
)
@@ -46,18 +45,15 @@ require (
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3 // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
+2 -21
View File
@@ -84,14 +84,12 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
goauthentik.io/api/v3 v3.2026020.16 h1:sEqcVRXYSJTYaSdU5PzSEdFUWDqCONm5BeL62F5k+58=
goauthentik.io/api/v3 v3.2026020.16/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
@@ -100,12 +98,6 @@ golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -119,21 +111,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.0.0-20260509204538-0dfb117cc6ec h1:xf12Yh3ltN4fnNyP0CyyM0TwNVnZDfLJjV3+bf9fPFY=
k8s.io/api v0.0.0-20260509204538-0dfb117cc6ec/go.mod h1:C+fcNlNQ9TcKHspN+DD7UybdfnjDAGyBjfCd6W7ogbY=
k8s.io/apimachinery v0.0.0-20260509204146-64dfe1db2af5 h1:k2HBxRBq6w2QCj14oAhBosjMqqgNlj4dmLXFj8f1A+8=
k8s.io/apimachinery v0.0.0-20260509204146-64dfe1db2af5/go.mod h1:37ALVDWo0LgW74Y9rAdewmZo20SVCGGH34806wUMrko=
k8s.io/apimachinery v0.0.0-20260513183604-f9371b815e42 h1:rWdGOTor3z0WSyZcRl9ms4dn9Cw9CqmNBqXuf2z0k1k=
k8s.io/apimachinery v0.0.0-20260513183604-f9371b815e42/go.mod h1:hiubQ6UTHIdr0bS8ExXOJEywFVOoudnldm/l/NiNVlA=
k8s.io/client-go v0.0.0-20260509205101-ca52b81a2940 h1:n5t5Jx3VpLdiAGxIvIHsZDmsExtZVwghUPLM3wFi6Go=
k8s.io/client-go v0.0.0-20260509205101-ca52b81a2940/go.mod h1:0e7OLwg7kdXISVFwn7ishFdvxfVgi7wsqHqsQPHl61w=
k8s.io/code-generator v0.0.0-20260509210052-5595d1310975 h1:hDrusFgTzvqcDJ7p13A9Eid4i8Y9uNSs/67lniaYHwM=
k8s.io/code-generator v0.0.0-20260509210052-5595d1310975/go.mod h1:mQXg0n0EeF4oU8aTwm9mzwoyAKqVRmUb9wLhjHnRq3I=
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ=
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM=
k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3/go.mod h1:yvyl3l9E+UxlqOMUULdKTAYB0rEhsmjr7+2Vb/1pCSo=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260509150519-312035bf509b h1:WrpNVPKkCaOO9h77E1P2HLnhWDQxrxzpf2jfsM8WevY=
k8s.io/kube-openapi v0.0.0-20260509150519-312035bf509b/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/kube-openapi v0.0.0-20260511211612-da4e56fe5676 h1:ahjrVu/DBcaAhw/GcblfaOvvQ2wi8kqXWvn62nud3UU=
k8s.io/kube-openapi v0.0.0-20260511211612-da4e56fe5676/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
@@ -142,8 +125,6 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo=
sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+40 -6
View File
@@ -17,10 +17,14 @@ limitations under the License.
package main
import (
"errors"
"flag"
"net/url"
"os"
"time"
"gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/signals"
authentikapi "goauthentik.io/api/v3"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
@@ -58,23 +62,29 @@ func main() {
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
operatorClient, err := clientset.NewForConfig(cfg)
proxyProviderClient, err := clientset.NewForConfig(cfg)
if err != nil {
logger.Error(err, "Error building kubernetes clientset")
logger.Error(err, "Error building proxy provider clientset")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
authentikClient, err := newAuthentikAPIClient(os.Getenv("AUTENTIK_HOST"), os.Getenv("AUTENTIK_TOKEN"))
if err != nil {
logger.Error(err, "Error building Authentik API client")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
operatorInformerFactory := informers.NewSharedInformerFactory(operatorClient, time.Second*30)
proxyProviderInformerFactory := informers.NewSharedInformerFactory(proxyProviderClient, time.Second*30)
controller := NewController(ctx, kubeClient, operatorClient,
controller := NewController(ctx, kubeClient, proxyProviderClient, authentikClient,
kubeInformerFactory.Apps().V1().Deployments(),
operatorInformerFactory.Proxyprovider().V1().ProxyProviders())
proxyProviderInformerFactory.Proxyprovider().V1().ProxyProviders())
// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(ctx.done())
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
kubeInformerFactory.Start(ctx.Done())
operatorInformerFactory.Start(ctx.Done())
proxyProviderInformerFactory.Start(ctx.Done())
if err = controller.Run(ctx, 2); err != nil {
logger.Error(err, "Error running controller")
@@ -86,3 +96,27 @@ func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}
// newAuthentikAPIClient builds the OpenAPI-generated goauthentik client when AUTENTIK_HOST is set.
func newAuthentikAPIClient(host, token string) (*authentikapi.APIClient, error) {
if host == "" {
return nil, errors.New("authentik host is not set")
}
cfg := authentikapi.NewConfiguration()
if u, err := url.Parse(host); err == nil && u.Host != "" {
cfg.Scheme = u.Scheme
if cfg.Scheme == "" {
cfg.Scheme = "https"
}
cfg.Host = u.Host
} else {
cfg.Scheme = "https"
cfg.Host = host
}
if token == "" {
return nil, errors.New("authentik token is not set")
}
cfg.AddDefaultHeader("Authorization", "Bearer "+token)
return authentikapi.NewAPIClient(cfg), nil
}
+1
View File
@@ -21,6 +21,7 @@ import (
)
// +genclient
// +kubebuilder:subresource:status
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ProxyProvider struct {