feat: User-defined profiles — named presets for policy, quota, scaling, and reconciler tuning#192
Merged
Merged
Conversation
ProfileRegistry is a top-level field on KatalogFile and Motif (profiles:)
holding user-defined named profiles for all six resource classes:
networkPolicies, resourceQuotas, limitRanges, hpa, pdb, rollingUpdate.
Each *ProfileDef type mirrors the inline fields of its TemplateSource
counterpart (minus declaration-level fields like name, namespace, labels,
conditions). Template expressions are allowed in all field values and are
resolved at reconcile time — validation skips any field containing {{ }}.
ProfileRegistry.Merge carries motif profiles into the katalog registry at
import time, returning a typed conflict error when the same name appears
in the same class in both registries.
Lookup methods are per-class (LookupNetworkPolicy, LookupResourceQuota, etc.)
and return (def, found) — callers check built-ins when found is false.
Named *ProfileEntry types (existing validation collector types) are unchanged.
validateUserProfiles (step 26 in ValidateConfig) checks the profiles: block before any profile reference is resolved: - Missing name → error - Duplicate name within a class → error - Shadowing a built-in name → warn via logger, allowed Each per-class profile reference validator (NetworkPolicy, ResourceQuota, HPA, PDB, RollingUpdate) now checks the katalog's user registry first. A profile name found in the user registry is accepted without consulting built-ins, so teams can define "medium" or "deny-all" with their own values. Unknown profile names are still rejected if absent from both the user registry and the built-in list — error messages unchanged. Tests in pkg/katalog and pkg/types cover: empty registry, valid entries, template expressions allowed, duplicate detection, missing-name detection, shadow-allowed, user profile accepted in reference validators, unknown still rejected, and cross-class same-name independence in Merge.
…oncile time
Each Apply*Profile function now accepts a ProfileRegistry as a second argument
and checks it before consulting the built-in map. User-defined profiles win on
name conflict — shadowing a built-in is allowed (warned at validate time).
Resolver gains profiles ProfileRegistry field with WithProfiles() setter and
Profiles() getter. The reconciler attaches the katalog's registry to the
resolver immediately after construction whenever the katalog declares a
profiles: block.
Every resource package Resolve() function accepts the registry and forwards it
to the Apply call:
networkpolicies, resourcequotas, pdbs, hpas,
deployments, replicasets, statefulsets
All seven runners pass resolver.Profiles() to their respective Resolve() call.
Existing profile tests updated to pass an empty ProfileRegistry{} — built-in
behavior is unchanged when no user profiles are declared.
…ict detection ExpandedMotif gains a Profiles ProfileRegistry field populated by Expand() from the motif's profiles: block, and a Name field for error messages. mergeExpandedMotif calls ProfileRegistry.Merge() after resource/status/admission merges. A naming conflict (same class, same profile name in both the motif and the katalog) is a hard error with a clear message. No conflict when the same name appears in different classes — class is the scope boundary. The merged registry is stored on k.Profiles and flows to the resolver via WithProfiles() at reconcile time (wired in the previous commit).
When a Motif declares a profiles: block, ork validate now prints a
profiles: line alongside resources: in the structured summary:
● tenant-isolation
Reusable network isolation motif
version : v0.2.0
inputs : 2
resources: networkPolicies(1) resourceQuotas(1)
profiles : networkPolicies(2) resourceQuotas(1)
Only non-empty classes are shown. The line is omitted entirely when the
motif declares no profiles.
documentation/concepts/profiles/10-user-defined-profiles.md — new page covering: - Declaring profiles at katalog root and in Motifs - Supported classes (networkPolicies, resourceQuotas, limitRanges, hpa, pdb, rollingUpdate) - Template expressions in profile field values (validated at reconcile time) - Validation rules: required name, uniqueness within class, shadowing allowed with warning - Resolution order: user-defined → motif-imported → built-in - Conflict detection on motif import (same class + name = hard error) - ork validate output showing profiles: count documentation/concepts/profiles/index.md — new row in the profile families table and a "User-defined" rule in the rules section. documentation/reference/schema/02-katalog/index.md — profiles: block added to the wire format example and linked in the reference table.
…er propagation
Two gaps closed:
1. LimitRange profile class was declared in ProfileRegistry but never wired:
- pkg/types/types_limitrange.go: add Profile field to LimitRangeTemplateSource
- pkg/types/hooks_limitrange_profile.go: LimitRangeProfileEntry + CollectLimitRangeProfileEntries()
- pkg/profiles/limitrange.go: ApplyLimitRangeProfile() (user-defined only — no built-ins)
- pkg/resources/limitranges/limitrange.go: Resolve() now accepts registry and applies profile
- pkg/runners/limitranges.go: pass resolver.Profiles() to Resolve()
- pkg/katalog/validate_limitrange_profile.go: validateLimitRangeProfiles()
- pkg/katalog/validate_user_profiles.go: isUserLimitRangeProfile() helper
- pkg/katalog/validate.go: call validateLimitRangeProfiles()
2. User-defined profiles declared in profiles: were never propagated from the merger
into the Katalog struct, so k.Profiles was always empty at validate time:
- pkg/merger/merger.go: add profiles field and ToProfiles()
- pkg/merger/file.go: set m.profiles = doc.Profiles in loadKatalog;
accumulate and merge from all imports in loadKomposer
- pkg/katalog/parser.go: k.Profiles = m.ToProfiles()
3. Example exercising all six user-defined profile classes:
- examples/use-cases/namespace-provisioner/03-user-defined-profiles/
katalog.yaml, cr.yaml, simulate.yaml
All six classes declared in profiles: and referenced from operatorBox.
ork validate passes on both katalog.yaml and simulate.yaml.
…iles as primary path The guide previously only covered adding built-in constants to existing classes. User-defined profiles (no code required, just profiles: in Katalog/Motif YAML) are now the primary recommendation. Built-in additions and new class wiring are documented as secondary and contributor paths respectively. Includes the correct profile field placement table (top-level vs behavior.profile vs rollingUpdate.profile per class) and the full 12-step checklist for wiring a new profile class end-to-end (derived from the LimitRange implementation).
Built-ins exist to work out of the box. The feature is designed for the profiles teams write themselves. A team writing profile: org-medium expresses an org contract; changing the definition propagates to every consumer without a grep or PR chain. That is what built-ins cannot do. Added a "Built-ins versus user-defined profiles" section with example YAML and the key resolution-order and conflict-detection properties.
- hpa: skip comparison for sides not explicitly set in desired spec;
Kubernetes injects default ScaleUp/SelectPolicy which caused a
continuous drift loop on every reconcile cycle
- limitrange: replace reflect.DeepEqual with Cmp()-based comparison;
Kubernetes normalises resource.Quantity values after round-trip,
causing false drift on every reconcile
- networkpolicies: translatePeer now defaults to empty podSelector when
all peer fields are nil after YAML bundle serialisation round-trip;
omitempty drops podSelector:{} which Kubernetes then rejects as
"must specify a peer"
- reconciler/run_status: skip PatchStatus when conditions and
observedGeneration are semantically unchanged; unconditional patch
was incrementing resourceVersion every reconcile, generating a watch
event that immediately re-queued the CR and defeated resync intervals
- katalog/serialize: include Profiles in SerializeExpanded output so
user-defined profiles survive the bundle ConfigMap round-trip and are
available to the runtime for validation and reconcile-time expansion
- katalog/generate_rbac: add escalate and bind verbs for
roles/clusterroles when detected in use; required by Kubernetes
privilege escalation prevention when the operator provisions RBAC on
behalf of tenant service accounts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces user-defined profiles — named presets that let teams codify their infrastructure patterns and reuse them across Katalogs and Motifs instead of using Orkestra built-in profiles.
Now you can declare profiles for all six profile classes directly in your Katalog or Motif:
networkPolicies— named network isolation patternsresourceQuotas— capacity tiers for teams or workloadslimitRanges— pod and container limitshpa— autoscaling behaviorspdb— pod disruption budgets behaviorsrollingUpdate— deployment update strategiesreconciler— controller tuning (workers, resync, queue depth)Example: Declaring profiles
What's changed
profiles:block now accepted at the root of any Katalog or MotifProfilefield, collector, validator, runner, andApplyLimitRangeProfile— previously had no profile support at allprofiles:declared in YAML was never reachingKatalog.Profiles— now wired throughMergerandKomposeRuntimeKatalogork validateoutput: Shows profile count per class in Motif outputsimulate.yaml; rootsimulate.yamlaggregator addedTest plan
make ork && make test-unitork validate -f examples/use-cases/profiles/09-user-defined/katalog.yamlork simulate -f examples/use-cases/profiles/simulate.yaml— all 9 passork validaterejects it