Skip to content

feat: User-defined profiles — named presets for policy, quota, scaling, and reconciler tuning#192

Merged
iAlexeze merged 18 commits into
mainfrom
feat/user-defined-profiles
Jun 28, 2026
Merged

feat: User-defined profiles — named presets for policy, quota, scaling, and reconciler tuning#192
iAlexeze merged 18 commits into
mainfrom
feat/user-defined-profiles

Conversation

@iAlexeze

@iAlexeze iAlexeze commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

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 patterns
  • resourceQuotas — capacity tiers for teams or workloads
  • limitRanges — pod and container limits
  • hpa — autoscaling behaviors
  • pdb — pod disruption budgets behaviors
  • rollingUpdate — deployment update strategies
  • reconciler — controller tuning (workers, resync, queue depth)

Example: Declaring profiles

apiVersion: orkestra.orkspace.io/v1
kind: Katalog
metadata:
  name: platform-operator

profiles:
  networkPolicies:
    - name: allow-monitoring
      description: Allow ingress from the platform monitoring namespace
      ingress:
        - from:
            - namespaceSelector:
                team: platform
      policyTypes: [Ingress]

  resourceQuotas:
    - name: team-medium
      description: Standard allocation for a medium-sized team namespace
      hard:
        pods: "30"
        cpu: "6"
        memory: "12Gi"
        requests.cpu: "3"
        requests.memory: "6Gi"

  reconciler:
    - name: api-service
      workers: 4
      resync: 30s
      queue:
        maxDepth: 200

spec:
  crds:
    namespace:
      operatorBox:
        reconciler:
          profile: api-service
      onCreate:
        networkPolicies:
          - name: "{{ .metadata.name }}-monitoring"
            profile: allow-monitoring
        resourceQuotas:
          - name: "{{ .metadata.name }}-quota"
            profile: team-medium

What's changed

  • Profile registry: profiles: block now accepted at the root of any Katalog or Motif
  • Resolution order: User-defined profiles resolve before built-ins; Motif-declared profiles merge into the importing Katalog's registry
  • Conflict detection: Duplicate names in the same class across Katalog and Motif are a hard error at load time
  • LimitRange wired end-to-end: Profile field, collector, validator, runner, and ApplyLimitRangeProfile — previously had no profile support at all
  • Merger propagation fixed: profiles: declared in YAML was never reaching Katalog.Profiles — now wired through Merger and KomposeRuntimeKatalog
  • ork validate output: Shows profile count per class in Motif output
  • Examples: 3 new use-case examples (06-networkpolicy, 07-resourcequota, 08-limitrange) plus 09-user-defined; all 9 have simulate.yaml; root simulate.yaml aggregator added
  • Documentation: User-defined profiles concept page, index and reference updated

Test plan

  • make ork && make test-unit
  • ork validate -f examples/use-cases/profiles/09-user-defined/katalog.yaml
  • ork simulate -f examples/use-cases/profiles/simulate.yaml — all 9 pass
  • Reference a user profile name that is not declared — ork validate rejects it
  • Declare the same profile name in a Katalog and an imported Motif — conflict error

iAlexeze added 18 commits June 26, 2026 16:54
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
@iAlexeze iAlexeze changed the title feat: user-defined profiles for all six profile classes feat: User-defined profiles — named presets for policy, quota, scaling, and reconciler tuning Jun 28, 2026
@iAlexeze iAlexeze merged commit 330b338 into main Jun 28, 2026
7 checks passed
@iAlexeze iAlexeze deleted the feat/user-defined-profiles branch June 28, 2026 02:28
@iAlexeze iAlexeze restored the feat/user-defined-profiles branch June 28, 2026 23:23
@iAlexeze iAlexeze deleted the feat/user-defined-profiles branch June 28, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant