marshall and unmarshall partially-defined yaml structure with custom yaml tags intact

62 views Asked by At

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:

  1. 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.
  2. 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.

0

There are 0 answers