diff --git a/go.mod b/go.mod index fdea3c4a..47078ff2 100644 --- a/go.mod +++ b/go.mod @@ -70,6 +70,7 @@ require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect @@ -77,6 +78,20 @@ require ( github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect + github.com/go-openapi/swag v0.27.1 // indirect + github.com/go-openapi/swag/cmdutils v0.27.1 // indirect + github.com/go-openapi/swag/conv v0.27.1 // indirect + github.com/go-openapi/swag/fileutils v0.27.1 // indirect + github.com/go-openapi/swag/jsonutils v0.27.1 // indirect + github.com/go-openapi/swag/loading v0.27.1 // indirect + github.com/go-openapi/swag/mangling v0.27.1 // indirect + github.com/go-openapi/swag/netutils v0.27.1 // indirect + github.com/go-openapi/swag/pools v0.27.1 // indirect + github.com/go-openapi/swag/stringutils v0.27.1 // indirect + github.com/go-openapi/swag/typeutils v0.27.1 // indirect + github.com/go-openapi/swag/yamlutils v0.27.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect @@ -142,6 +157,7 @@ require ( golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.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 gotest.tools/v3 v3.5.2 // indirect k8s.io/klog/v2 v2.140.0 // indirect diff --git a/go.sum b/go.sum index 2a5d44ce..98da11cc 100644 --- a/go.sum +++ b/go.sum @@ -138,6 +138,8 @@ github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzr github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU= github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY= github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE= github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA= @@ -152,6 +154,10 @@ github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71U github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA= github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= diff --git a/internal/service/kubernetes_crd_extractor.go b/internal/service/kubernetes_crd_extractor.go index 8cd66791..633f15e3 100644 --- a/internal/service/kubernetes_crd_extractor.go +++ b/internal/service/kubernetes_crd_extractor.go @@ -1,22 +1,29 @@ package service import ( + "context" + "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/pkg/apis/tinyauth/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) type KubernetesCRDInput struct { - Log *logger.Logger + Log *logger.Logger + Client kubernetes.Interface } type KubernetesCRDExtractor struct { - log *logger.Logger + log *logger.Logger + client kubernetes.Interface } func NewKubernetesCRDExtractor(i KubernetesCRDInput) *KubernetesCRDExtractor { return &KubernetesCRDExtractor{ - log: i.Log, + log: i.Log, + client: i.Client, } } @@ -48,11 +55,29 @@ func (k *KubernetesCRDExtractor) Extract(app *v1alpha1.Application) ExtractionRe } } + // Convert the CRD to the internal representation. + internalApp := app.Spec.ToInternalApp() + passwordRef := app.Spec.Response.BasicAuth.PasswordSecretRef + if passwordRef != nil { + secret, err := k.client.CoreV1().Secrets(meta.Namespace).Get(context.Background(), passwordRef.Name, metav1.GetOptions{}) + if err != nil { + k.log.App.Warn().Err(err).Str("namespace", meta.Namespace).Str("name", meta.Name).Str("secret", passwordRef.Name).Str("key", passwordRef.Key).Msg("Failed to read basic auth password Secret, skipping") + return ExtractionResult{Meta: meta} + } + + password, ok := secret.Data[passwordRef.Key] + if !ok { + k.log.App.Warn().Str("namespace", meta.Namespace).Str("name", meta.Name).Str("secret", passwordRef.Name).Str("key", passwordRef.Key).Msg("Basic auth password Secret key does not exist, skipping") + return ExtractionResult{Meta: meta} + } + + internalApp.Response.BasicAuth.Password = string(password) + } + return ExtractionResult{ Meta: meta, Apps: map[string]model.App{ - // Convert the CRD to the internal representation - meta.Name: app.Spec.ToInternalApp(), + meta.Name: internalApp, }, } } diff --git a/internal/service/kubernetes_service.go b/internal/service/kubernetes_service.go index f5e2edf9..83e88db5 100644 --- a/internal/service/kubernetes_service.go +++ b/internal/service/kubernetes_service.go @@ -18,6 +18,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) @@ -114,10 +115,11 @@ func (ti *typedItem) fromUnstructured(typ ResourceType, obj *unstructured.Unstru type KubernetesService struct { log *logger.Logger - apps map[ResourceMeta]map[string]model.App - client dynamic.Interface - mu sync.RWMutex - connected bool + apps map[ResourceMeta]map[string]model.App + client dynamic.Interface + typedClient kubernetes.Interface + mu sync.RWMutex + connected bool } type KubernetesServiceInput struct { @@ -138,11 +140,16 @@ func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error) if err != nil { return nil, fmt.Errorf("failed to create kubernetes client: %w", err) } + typedClient, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create typed kubernetes client: %w", err) + } service := &KubernetesService{ - log: i.Log, - client: client, - apps: make(map[ResourceMeta]map[string]model.App), + log: i.Log, + client: client, + typedClient: typedClient, + apps: make(map[ResourceMeta]map[string]model.App), } watchedGVRs := make(map[string]bool) @@ -226,7 +233,8 @@ func (k *KubernetesService) watchedItemChange(res watchedResource, typedItem *ty return } extractor := NewKubernetesCRDExtractor(KubernetesCRDInput{ - Log: k.log, + Log: k.log, + Client: k.typedClient, }) result = extractor.Extract(typedItem.crd) } diff --git a/pkg/apis/tinyauth/v1alpha1/application.go b/pkg/apis/tinyauth/v1alpha1/application.go index 6b389ac2..22b9d0df 100644 --- a/pkg/apis/tinyauth/v1alpha1/application.go +++ b/pkg/apis/tinyauth/v1alpha1/application.go @@ -1,6 +1,9 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) //go:generate controller-gen object paths=$GOFILE @@ -81,9 +84,8 @@ type AppResponse struct { // AppBasicAuth specifies basic auth credentials type AppBasicAuth struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - PasswordFile string `json:"passwordFile,omitempty"` + Username string `json:"username,omitempty"` + PasswordSecretRef *corev1.SecretKeySelector `json:"passwordSecretRef,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/apis/tinyauth/v1alpha1/crds/tinyauth.app_applications.yaml b/pkg/apis/tinyauth/v1alpha1/crds/tinyauth.app_applications.yaml index a3657463..440d6db8 100644 --- a/pkg/apis/tinyauth/v1alpha1/crds/tinyauth.app_applications.yaml +++ b/pkg/apis/tinyauth/v1alpha1/crds/tinyauth.app_applications.yaml @@ -48,11 +48,8 @@ spec: properties: domain: type: string - name: - type: string required: - domain - - name type: object ip: description: AppIP specifies IP access control rules @@ -99,10 +96,30 @@ spec: basicAuth: description: AppBasicAuth specifies basic auth credentials properties: - password: - type: string - passwordFile: - type: string + passwordSecretRef: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic username: type: string type: object diff --git a/pkg/apis/tinyauth/v1alpha1/mapper.go b/pkg/apis/tinyauth/v1alpha1/mapper.go index bf8d452f..ac1c46e3 100644 --- a/pkg/apis/tinyauth/v1alpha1/mapper.go +++ b/pkg/apis/tinyauth/v1alpha1/mapper.go @@ -26,9 +26,7 @@ func (s *ApplicationSpec) ToInternalApp() model.App { Response: model.AppResponse{ Headers: s.Response.Headers, BasicAuth: model.AppBasicAuth{ - Username: s.Response.BasicAuth.Username, - Password: s.Response.BasicAuth.Password, - PasswordFile: s.Response.BasicAuth.PasswordFile, + Username: s.Response.BasicAuth.Username, }, }, Path: model.AppPath{ diff --git a/pkg/apis/tinyauth/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/tinyauth/v1alpha1/zz_generated.deepcopy.go index 35bca166..f0c1e60e 100644 --- a/pkg/apis/tinyauth/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/tinyauth/v1alpha1/zz_generated.deepcopy.go @@ -5,12 +5,18 @@ package v1alpha1 import ( + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppBasicAuth) DeepCopyInto(out *AppBasicAuth) { *out = *in + if in.PasswordSecretRef != nil { + in, out := &in.PasswordSecretRef, &out.PasswordSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppBasicAuth. @@ -121,7 +127,7 @@ func (in *AppResponse) DeepCopyInto(out *AppResponse) { *out = make([]string, len(*in)) copy(*out, *in) } - out.BasicAuth = in.BasicAuth + in.BasicAuth.DeepCopyInto(&out.BasicAuth) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppResponse.