From 539b172804af66d4b6551fef48af7638c240d95d Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:38:53 +0100 Subject: [PATCH] add OIDC datagatherer Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- .../disco-agent/templates/configmap.yaml | 2 + deploy/charts/disco-agent/templates/rbac.yaml | 16 ++- examples/one-shot-oidc.yaml | 16 +++ pkg/agent/config.go | 3 + pkg/datagatherer/oidc/oidc.go | 115 ++++++++++++++++++ 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 examples/one-shot-oidc.yaml create mode 100644 pkg/datagatherer/oidc/oidc.go diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 231a26cd..4766e762 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -19,6 +19,8 @@ data: {{- . | toYaml | nindent 6 }} {{- end }} data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic diff --git a/deploy/charts/disco-agent/templates/rbac.yaml b/deploy/charts/disco-agent/templates/rbac.yaml index f3fae414..cc8ca8aa 100644 --- a/deploy/charts/disco-agent/templates/rbac.yaml +++ b/deploy/charts/disco-agent/templates/rbac.yaml @@ -95,4 +95,18 @@ subjects: - kind: ServiceAccount name: {{ include "disco-agent.serviceAccountName" . }} namespace: {{ .Release.Namespace }} - +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "disco-agent.fullname" . }}-oidc-discovery + labels: + {{- include "disco-agent.labels" . | nindent 4 }} +roleRef: + kind: ClusterRole + name: system:service-account-issuer-discovery + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "disco-agent.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/examples/one-shot-oidc.yaml b/examples/one-shot-oidc.yaml new file mode 100644 index 00000000..d5ce8e99 --- /dev/null +++ b/examples/one-shot-oidc.yaml @@ -0,0 +1,16 @@ +# one-shot-oidc.yaml +# +# An example configuration file which can be used for local testing. +# For example: +# +# go run . agent \ +# --agent-config-file examples/one-shot-oidc.yaml \ +# --one-shot \ +# --output-path output.json +# +organization_id: "my-organization" +cluster_id: "my_cluster" +period: 1m +data-gatherers: +- kind: "oidc" + name: "ark/oidc" diff --git a/pkg/agent/config.go b/pkg/agent/config.go index ed72afe4..b63e09cf 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -22,6 +22,7 @@ import ( "github.com/jetstack/preflight/pkg/datagatherer/k8sdiscovery" "github.com/jetstack/preflight/pkg/datagatherer/k8sdynamic" "github.com/jetstack/preflight/pkg/datagatherer/local" + "github.com/jetstack/preflight/pkg/datagatherer/oidc" "github.com/jetstack/preflight/pkg/kubeconfig" "github.com/jetstack/preflight/pkg/logs" "github.com/jetstack/preflight/pkg/version" @@ -901,6 +902,8 @@ func (dg *DataGatherer) UnmarshalYAML(unmarshal func(any) error) error { cfg = &k8sdynamic.ConfigDynamic{} case "k8s-discovery": cfg = &k8sdiscovery.ConfigDiscovery{} + case "oidc": + cfg = &oidc.OIDCDiscovery{} case "local": cfg = &local.Config{} // dummy dataGatherer is just used for testing diff --git a/pkg/datagatherer/oidc/oidc.go b/pkg/datagatherer/oidc/oidc.go new file mode 100644 index 00000000..d68fbfc9 --- /dev/null +++ b/pkg/datagatherer/oidc/oidc.go @@ -0,0 +1,115 @@ +package oidc + +import ( + "context" + "encoding/json" + "fmt" + + "k8s.io/client-go/rest" + + "github.com/jetstack/preflight/pkg/datagatherer" + "github.com/jetstack/preflight/pkg/kubeconfig" +) + +// OIDCDiscovery contains the configuration for the k8s-discovery data-gatherer +type OIDCDiscovery struct { + // KubeConfigPath is the path to the kubeconfig file. If empty, will assume it runs in-cluster. + KubeConfigPath string `yaml:"kubeconfig"` +} + +// UnmarshalYAML unmarshals the Config resolving GroupVersionResource. +func (c *OIDCDiscovery) UnmarshalYAML(unmarshal func(any) error) error { + aux := struct { + KubeConfigPath string `yaml:"kubeconfig"` + }{} + err := unmarshal(&aux) + if err != nil { + return err + } + + c.KubeConfigPath = aux.KubeConfigPath + + return nil +} + +func (c *OIDCDiscovery) NewDataGatherer(ctx context.Context) (datagatherer.DataGatherer, error) { + cl, err := kubeconfig.NewDiscoveryClient(c.KubeConfigPath) + if err != nil { + return nil, err + } + + return &DataGathererOIDC{ + cl: cl.RESTClient(), + }, nil +} + +// DataGathererOIDC stores the config for a k8s-discovery datagatherer +type DataGathererOIDC struct { + cl rest.Interface +} + +func (g *DataGathererOIDC) Run(ctx context.Context) error { + return nil +} + +func (g *DataGathererOIDC) WaitForCacheSync(ctx context.Context) error { + // no async functionality, see Fetch + return nil +} + +// Fetch will fetch discovery data from the apiserver, or return an error +func (g *DataGathererOIDC) Fetch() (any, int, error) { + ctx := context.Background() + + oidcResponse, oidcErr := g.fetchOIDCConfig(ctx) + jwksResponse, jwksErr := g.fetchJWKS(ctx) + + errToString := func(err error) string { + if err != nil { + return err.Error() + } + return "" + } + + return OIDCDiscoveryData{ + OIDCConfig: oidcResponse, + OIDCConfigError: errToString(oidcErr), + JWKS: jwksResponse, + JWKSError: errToString(jwksErr), + }, 1, nil +} + +type OIDCDiscoveryData struct { + OIDCConfig map[string]any `json:"openid_configuration,omitempty"` + OIDCConfigError string `json:"openid_configuration_error,omitempty"` + JWKS map[string]any `json:"jwks,omitempty"` + JWKSError string `json:"jwks_error,omitempty"` +} + +func (g *DataGathererOIDC) fetchOIDCConfig(ctx context.Context) (map[string]any, error) { + bytes, err := g.cl.Get().AbsPath("/.well-known/openid-configuration").Do(ctx).Raw() + if err != nil { + return nil, fmt.Errorf("failed to get OIDC discovery document: %v", err) + } + + var oidcResponse map[string]any + if err := json.Unmarshal(bytes, &oidcResponse); err != nil { + return nil, fmt.Errorf("failed to unmarshal OIDC discovery document: %v", err) + } + + return oidcResponse, nil +} + +func (g *DataGathererOIDC) fetchJWKS(ctx context.Context) (map[string]any, error) { + bytes, err := g.cl.Get().AbsPath("/openid/v1/jwks").Do(ctx).Raw() + if err != nil { + return nil, fmt.Errorf("failed to get JWKS from jwks_uri: %v", err) + } + + var jwksResponse map[string]any + if err := json.Unmarshal(bytes, &jwksResponse); err != nil { + return nil, fmt.Errorf("failed to unmarshal JWKS response: %v", err) + } + + return jwksResponse, nil +}