I am trying to marshal and unmarshal an authentik blueprint into a custom resource for a Kubernetes operator (operator SDK / controller runtime). There are a few constraints:
- Authentik blueprints can vary substantially in certain areas like
.entries.attrs
we do not want to define a set schema under this key as it is too varied to be maintainable. - Authentik blueprints use custom yaml tags like
!KeyOf
and!Find
which should be maintained as is without changing the subsequent arguments like!Find [authentik_flows.flow, [slug, default-password-change]]
should stay as is after unmarshall + marshall.
Here is an example authentik blueprint:
version: 1
metadata:
name: Default - Authentication flow
entries:
- attrs:
backends:
- authentik.core.auth.InbuiltBackend
- authentik.sources.ldap.auth.LDAPBackend
- authentik.core.auth.TokenBackend
configure_flow: !Find [authentik_flows.flow, [slug, default-password-change]]
identifiers:
name: default-authentication-password
id: default-authentication-password
model: authentik_stages_password.passwordstage
- attrs:
user_fields:
- email
- username
identifiers:
name: default-authentication-identification
id: default-authentication-identification
model: authentik_stages_identification.identificationstage
- identifiers:
name: default-authentication-login
id: default-authentication-login
model: authentik_stages_user_login.userloginstage
- identifiers:
order: 10
stage: !KeyOf default-authentication-identification
target: !KeyOf flow
model: authentik_flows.flowstagebinding
To solve issue 1. I tried defining a schema that used json.RawMessage
as the type of highly uncertain keys like .entries.attrs
. However I found that json.RawMessage
would strip and unpack the custom yaml tags from constraint 2. .
I have also been experimenting with yaml.Node
(from go-yaml v3 but does not have DeepCopyInto
type function), []byte
, interface{}
and map[string]interface{}
.
Here is my current schema that succeeds at solving issue 1. but is disastrous for issue 2. (the full code is available here if more detail is needed):
import (
"encoding/json"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
...
// BP is a whole blueprint struct containing the full structure of an authentik blueprint
// https://goauthentik.io/developer-docs/blueprints/v1/structure#structure
type BP struct {
//+kubebuilder:default=1
// Version is the version of this blueprint
Version int `json:"version"`
// Metadata block specifying labels and names of the blueprint
Metadata BPMeta `json:"metadata"`
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
// Context (optional) authentik default context (whatever that means)
Context json.RawMessage `json:"context,omitempty"`
// +kubebuilder:validation:MinItems=1
// Entries lists models we want to use via this blueprint
Entries []BPModel `json:"entries"`
}
// BPMeta is the metadata of an authentik blueprint as authentik likes
type BPMeta struct {
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
// Labels (optional) key-value store for special labels
// https://goauthentik.io/developer-docs/blueprints/v1/structure#special-labels
Labels json.RawMessage `json:"labels,omitempty"`
// Name of the authentik blueprint for authentik to register
Name string `json:"name"`
}
// BPModel is a rough outline of the structure of models authentik likes in its blueprints
type BPModel struct {
// Model "app.model" notation of which model from authentik to call
Model string `json:"model"`
//+kubebuilder:validation:Enum="present";"create";"absent"
//+kubebuilder:default:=present
// State (optional) desired state of this model when loaded from "present", "create", "absent"
// present: (default) keeps the object in sync with its definition in this blueprint
// create: only creates the initial object with its values here
// absent: deletes the object
State string `json:"state,omitempty"`
// Conditions (optional) a list of conditions which if all match the model will be activated. If not the model will be inactive
Conditions []string `json:"conditions,omitempty"`
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
// Identifiers key-value identifiers to allow filtering of this stage, and identifying it
Identifiers json.RawMessage `json:"identifiers"`
// Id (optional) is similar to identifiers except is optional and is just an ID to reference this model using !KeyOf syntax in authentik
Id string `json:"id,omitempty"`
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
// Attrs is a map of settings / options / overrides of the defaults of this model
Attrs json.RawMessage `json:"attrs,omitempty"`
}
I have been tearing my hair out trying to find a good solution, any help would be much appreciated.