Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bc046f2
feat(types): add ProfileRegistry and per-class ProfileDef types
iAlexeze Jun 26, 2026
36c2e3b
feat(katalog): validate user-defined profiles at load time
iAlexeze Jun 26, 2026
7e844cc
feat(profiles): resolve user-defined profiles before built-ins at rec…
iAlexeze Jun 26, 2026
423f15a
feat(motif): carry profiles through motif import and merge with confl…
iAlexeze Jun 26, 2026
11e092f
feat(cli): show profile counts in ork validate Motif output
iAlexeze Jun 26, 2026
10682ca
docs: user-defined profiles concept page and reference updates
iAlexeze Jun 26, 2026
c1f6262
feat(profiles): wire LimitRange profile class end-to-end and fix merg…
iAlexeze Jun 26, 2026
df8ee06
docs(profiles): rewrite adding-a-profile guide with user-defined prof…
iAlexeze Jun 26, 2026
d850edb
docs(profiles): explain user-defined profiles vs built-ins in the index
iAlexeze Jun 26, 2026
6f73f96
docs: add language tags to bare opening code fences
iAlexeze Jun 26, 2026
fe478ab
scripts: add fix-bare-fences.py to enforce language tags on opening c…
iAlexeze Jun 26, 2026
c91f7fa
build: run fix-bare-fences.py on documentation/ after ork build
iAlexeze Jun 26, 2026
7b8b196
style: gofmt types_profiles.go
iAlexeze Jun 26, 2026
328adad
docs(profiles): add merger propagation step to new profile class chec…
iAlexeze Jun 26, 2026
596e3a4
docs(profiles): expand step 9 to cover all three validate_user_profil…
iAlexeze Jun 26, 2026
e89aa36
docs(profiles): fix broken link to user-defined profiles concept page
iAlexeze Jun 26, 2026
6a29f9e
fix: runtime stability and bundle correctness
iAlexeze Jun 27, 2026
a90b32f
update the profiles examples with simulate.yaml each, and add user de…
iAlexeze Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ ork: generate-notes
cd $(ORKESTRA_DIR) && gofmt -w .
cd $(ORKESTRA_DIR) && go build -ldflags "$(ORK_LDFLAGS)" -o $(OUTPUT_DIR)/ork ./cmd/orkestra
@echo "✅ Orkestra built successfully"
@python3 scripts/fix-bare-fences.py documentation/

orkcc:
@echo "Building Orkestra Control Center..."
Expand Down
25 changes: 25 additions & 0 deletions cmd/cli/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@ func validateMotifFile(path string) error {
if summary := motifResourceSummary(m); summary != "" {
fmt.Printf(" %s\n", gray("resources: "+summary))
}
if summary := motifProfileSummary(m); summary != "" {
fmt.Printf(" %s\n", gray("profiles : "+summary))
}
}

fmt.Println()
Expand All @@ -529,6 +532,28 @@ func validateMotifFile(path string) error {
return nil
}

// motifProfileSummary returns a compact string listing non-empty profile classes
// and their counts, e.g. "networkPolicies(2) resourceQuotas(1)".
func motifProfileSummary(m *orktypes.Motif) string {
reg := m.Profiles
if reg.IsEmpty() {
return ""
}
var parts []string
add := func(kind string, n int) {
if n > 0 {
parts = append(parts, fmt.Sprintf("%s(%d)", kind, n))
}
}
add("networkPolicies", len(reg.NetworkPolicies))
add("resourceQuotas", len(reg.ResourceQuotas))
add("limitRanges", len(reg.LimitRanges))
add("hpa", len(reg.HPA))
add("pdb", len(reg.PDB))
add("rollingUpdate", len(reg.RollingUpdate))
return strings.Join(parts, " ")
}

// motifResourceSummary returns a compact string listing non-empty resource types
// and their counts, e.g. "deployments(1) services(1) networkPolicies(2)".
func motifResourceSummary(m *orktypes.Motif) string {
Expand Down
174 changes: 174 additions & 0 deletions documentation/concepts/profiles/10-user-defined-profiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# User-Defined Profiles

Built-in Orkestra profiles cover the common cases. User-defined profiles let your team define the cases specific to you — your network topology, your compliance requirements, your capacity tiers — and name them so that intent travels through every Katalog and Motif that imports them.

---

## Declaring profiles

Profiles are declared at the root of a Katalog or Motif alongside `spec:` and `security:`:

```yaml
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"

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

---

## Supported profile classes

| Class | YAML key | Expands into |
|-------|----------|--------------|
| NetworkPolicy | `profiles.networkPolicies` | ingress/egress rules, policyTypes |
| ResourceQuota | `profiles.resourceQuotas` | hard limits map |
| LimitRange | `profiles.limitRanges` | limit items |
| HPA | `profiles.hpa` | minReplicas, maxReplicas, CPU target, behavior |
| PDB | `profiles.pdb` | minAvailable or maxUnavailable |
| Rolling Update | `profiles.rollingUpdate` | maxSurge, maxUnavailable |

---

## Template expressions in profile fields

Profile field values support template expressions. They are resolved at reconcile time against the live CR:

```yaml
profiles:
resourceQuotas:
- name: cr-sized
hard:
pods: "{{ .spec.maxPods }}"
cpu: "{{ .spec.cpuLimit }}"
memory: "{{ .spec.memLimit }}"

hpa:
- name: cr-scaled
minReplicas: "{{ .spec.minReplicas | default \"2\" }}"
maxReplicas: "{{ .spec.maxReplicas }}"
targetCPUUtilizationPercentage: "70"
```

At `ork validate` time, fields containing `{{` are skipped — they cannot be validated statically. At reconcile time, the expression is expanded before the profile is applied.

---

## Validation

`ork validate` enforces three rules on the `profiles:` block:

1. **Non-empty name** — every profile entry must declare a `name`.
2. **Unique within class** — two `networkPolicies` entries with the same name is an error. Two entries with the same name in different classes (`resourceQuotas` and `hpa`) is fine — class is the scope boundary.
3. **Shadowing built-ins** — allowed but warned. If you declare a `networkPolicies` profile named `deny-all`, your version is used instead of Orkestra's built-in. A warning is printed at validate time so the shadowing is explicit.

---

## Resolution order

When a resource declares `profile: some-name`, Orkestra resolves it in this order:

1. User-defined profiles in the katalog `profiles:` block
2. User-defined profiles merged from imported Motifs
3. Built-in Orkestra profiles

The first match wins. Built-ins are only consulted when the name is not found in any user registry.

---

## Profiles in Motifs

A Motif can declare its own `profiles:` block. When a Katalog imports the Motif, its profiles are merged into the Katalog's registry:

```yaml
# tenant-isolation.motif.yaml
apiVersion: orkestra.orkspace.io/v1
kind: Motif
metadata:
name: tenant-isolation
version: v0.2.0

profiles:
networkPolicies:
- name: allow-monitoring
ingress:
- from:
- namespaceSelector:
team: platform
policyTypes: [Ingress]
- name: allow-internal
ingress:
- from:
- namespaceSelector:
scope: internal
policyTypes: [Ingress]

resources:
networkPolicies:
- name: "{{ .metadata.name }}-deny-all"
profile: deny-all
- name: "{{ .metadata.name }}-monitoring"
profile: allow-monitoring
```

### Conflict detection

If the same profile name appears in the same class in both the Katalog and an imported Motif — or in two imported Motifs — it is a **hard error** at load time:

```text
profile conflict: networkPolicies "allow-monitoring" defined in both motif "tenant-isolation" and the katalog
```

The same name in different classes is not a conflict — `resourceQuotas.medium` and `hpa.medium` are independent.

---

## ork validate output

When a Motif declares profiles, `ork validate` shows them alongside resources:

```text
● tenant-isolation
Reusable network isolation motif
version : v0.2.0
inputs : 2
resources: networkPolicies(1)
profiles : networkPolicies(2)
```

---

→ Back: [09 — NetworkPolicy Profile](09-networkpolicy-profile.md) | [Profiles index](index.md)
42 changes: 42 additions & 0 deletions documentation/concepts/profiles/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,46 @@ Static names are validated at load time. Template expressions are validated when

---

## Built-ins versus user-defined profiles

Orkestra ships with built-in profiles — `deny-all`, `small/medium/large/xlarge`, `safe`, `zero-downtime` — so the feature works immediately without any extra YAML. They cover common Kubernetes patterns.

But the feature is designed for the profiles you write yourself.

A team writing `profile: org-medium` is expressing an organizational contract: this namespace gets the capacity we agreed on for medium-sized teams. The numbers follow from that decision. Change the `org-medium` definition once and it propagates to every Katalog and Motif that references it — no grep, no PR chain, no drift.

That is what built-ins cannot do. `medium` is Orkestra's guess at what medium means. `org-medium` is your team's actual answer.

User-defined profiles are declared in a `profiles:` block at the root of any Katalog or Motif:

```yaml
profiles:
resourceQuotas:
- name: org-medium
description: Standard allocation for a medium-sized team namespace
hard:
pods: "25"
cpu: "4"
memory: "8Gi"

networkPolicies:
- name: org-deny-all
description: Block all traffic — start here and add policies for what you need
policyTypes: [Ingress, Egress]

rollingUpdate:
- name: org-safe
description: Never reduces capacity during a rollout
maxSurge: "1"
maxUnavailable: "0"
```

They resolve before built-ins. A profile named `deny-all` in your `profiles:` block shadows the built-in. A conflict between two imported Motifs declaring the same profile name in the same class is a hard error at load time.

See [User-defined profiles](./10-user-defined-profiles.md) for the full reference.

---

## Profile families

| Family | What it controls | Applied to |
Expand All @@ -75,6 +115,7 @@ Static names are validated at load time. Template expressions are validated when
| [Rolling Update](./07-rolling-update-profile.md) | Deployment/StatefulSet/ReplicaSet rollout strategy | `deployments[*].rollingUpdate`, `statefulSets[*].rollingUpdate`, `replicaSets[*].rollingUpdate` |
| [ResourceQuota](./08-resourcequota-profile.md) | Namespace resource limits (pods, CPU, memory) | `resourceQuotas[*]` |
| [NetworkPolicy](./09-networkpolicy-profile.md) | Traffic allow/deny rules for pods | `networkPolicies[*]` |
| [User-defined](./10-user-defined-profiles.md) | Custom named profiles declared in your Katalog or Motif | All profile-supporting fields |

---

Expand All @@ -84,3 +125,4 @@ Static names are validated at load time. Template expressions are validated when
- **Fail-fast** — an unknown profile name is a Katalog load error, not a runtime error.
- **No mixing** — a profile and explicit fields of the same type cannot coexist on the same resource.
- **Template-safe** — profile names can be template expressions. Static names are validated immediately; template expressions are validated at reconcile time.
- **User-defined** — teams can declare custom named profiles in a Katalog or Motif `profiles:` block. They resolve before built-ins. See [User-defined profiles](./10-user-defined-profiles.md).
10 changes: 10 additions & 0 deletions documentation/reference/schema/02-katalog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ spec:
imports: # Motif imports
...

profiles: # optional — user-defined named profiles
networkPolicies:
- name: allow-monitoring
...
resourceQuotas:
- name: team-medium
...

security: # optional
...

Expand Down Expand Up @@ -80,6 +88,8 @@ This scaffolds the simplest Katalog — a single CRD that creates a Deployment a
| [10-katalog-security.md](10-katalog-security.md) | `security` block |
| [11-katalog-notification.md](11-katalog-notification.md) | `notification` block |
| [12-katalog-providers.md](12-katalog-providers.md) | `providers` block |
| [16-resource-types.md](16-resource-types.md) | Supported Kubernetes resource types |
| [Profiles concept](../../../concepts/profiles/10-user-defined-profiles.md) | `profiles:` — user-defined named profiles |
| [15-enrich.md](15-enrich.md) | `enrich` — post-reconcile enrichment |
| [16-resource-types.md](16-resource-types.md) | Supported resource types and placeholder fields |

Expand Down
2 changes: 1 addition & 1 deletion examples/beginner/03-secret-copy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ ork simulate

Because the Secret copy requires reading the source Secret from a live cluster, Orkestra detects this automatically and skips it during simulation:

```
```text
note: secrets/{{ .spec.secretName }}: cross-namespace copy skipped in simulate — requires a live cluster

Cycle 1:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: provisioner.orkestra.io/v1
kind: NamespaceClaim
metadata:
name: team-gamma-claim
spec:
targetNamespace: team-gamma
team: gamma
owner: gamma-operator
ownerNamespace: default
tier: medium
Loading
Loading