Skip to content

Commit bc62e67

Browse files
docs: document Trait removes and toEndpointResources macro (#716)
- Add a "Removing Resources" section to the Patching Syntax page covering the Trait spec.removes section: target shape, execution order, the workload-kind removal restriction, and forEach removal. - Document the opt-in workload.toEndpointResources() CEL macro under Workload Helpers, with route fields and gRPC/HTTP examples, and link to it from the workload context-variables reference. Signed-off-by: Chathuranga Dassanayake <chathura2ihl@gmail.com>
1 parent 88d0dee commit bc62e67

6 files changed

Lines changed: 286 additions & 0 deletions

File tree

docs/platform-engineer-guide/component-types/patching-syntax.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Traits can modify existing resources using patches, which are JSON Patch operati
1515
- CEL-based resource targeting
1616
- forEach iteration support
1717

18+
A Trait can also **delete** whole resources produced by the ComponentType or earlier traits using the `spec.removes` section. See [Removing Resources](#removing-resources).
19+
1820
## Basic Patch Structure
1921

2022
Patches are defined in the Trait's `spec.patches` section:
@@ -272,6 +274,73 @@ patches:
272274
name: ${port.name}
273275
```
274276

277+
## Removing Resources
278+
279+
Patches modify resources in place. When a Trait needs to **delete** a whole resource produced by the ComponentType or by an earlier trait, use the `spec.removes` section instead. Each entry matches resources by GVK (with an optional `where` filter) and removes the matched resources entirely from the rendered output.
280+
281+
```yaml
282+
apiVersion: openchoreo.dev/v1alpha1
283+
kind: Trait
284+
metadata:
285+
name: drop-default-route
286+
spec:
287+
removes:
288+
- target:
289+
kind: HTTPRoute
290+
group: gateway.networking.k8s.io
291+
version: v1
292+
targetPlane: dataplane
293+
```
294+
295+
A remove entry uses the same `target` shape as a patch, but has no `operations`. The whole matched resource is deleted:
296+
297+
| Field | Required | Description |
298+
| ----------------- | -------- | ---------------------------------------------------------------------- |
299+
| `target.kind` | Yes | Resource kind to remove (e.g. `HTTPRoute`, `ConfigMap`) |
300+
| `target.group` | Yes | API group; use `""` for core API resources |
301+
| `target.version` | Yes | API version (e.g. `v1`) |
302+
| `target.where` | No | CEL expression filtering which matching resources are removed |
303+
| `targetPlane` | No | Plane whose resources are targeted; defaults to `"dataplane"` |
304+
| `forEach` / `var` | No | Iterate over a CEL list, binding each item to `var` for use in `where` |
305+
306+
**Execution order** - Within a single trait, removes run **after** its `creates` and `patches`. This lets one trait fully express a substitution: create a replacement resource and then remove the original.
307+
308+
```yaml
309+
spec:
310+
creates:
311+
- template:
312+
apiVersion: v1
313+
kind: ConfigMap
314+
metadata:
315+
name: ${metadata.name}-tuned-config
316+
data:
317+
mode: optimized
318+
removes:
319+
# Drop the ConfigMap the ComponentType emitted, now that the tuned one exists
320+
- target:
321+
kind: ConfigMap
322+
group: ""
323+
version: v1
324+
where: ${resource.metadata.name == metadata.name + "-default-config"}
325+
```
326+
327+
:::warning Workload resources cannot be removed
328+
The primary workload is defined by the ComponentType, so traits **must not** delete it. The admission webhook rejects removes that target a built-in workload GVK: kinds `Deployment`, `StatefulSet`, `DaemonSet`, `CronJob`, or `Job` in the `apps` or `batch` groups. The match is on the full GVK, so a custom CRD that merely shares one of these kind names in a different group (e.g. `group: example.com`, `kind: Deployment`) is **not** rejected.
329+
:::
330+
331+
`forEach` is supported just like in patches, letting you remove a set of resources derived from a CEL list:
332+
333+
```yaml
334+
removes:
335+
- target:
336+
kind: HTTPRoute
337+
group: gateway.networking.k8s.io
338+
version: v1
339+
where: ${resource.metadata.labels["openchoreo.dev/endpoint-name"] == route}
340+
forEach: ${parameters.routesToDrop}
341+
var: route
342+
```
343+
275344
## Path Resolution Behavior
276345

277346
| Path Type | Operation | Behavior |

docs/reference/cel/context-variables.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,13 @@ containers:
192192
port: ${workload.endpoints[endpoint].port}
193193
```
194194

195+
**Helper methods:** The `workload` object exposes two endpoint helpers:
196+
197+
- `workload.toServicePorts()`: converts the endpoints map into Kubernetes Service ports.
198+
- `workload.toEndpointResources(endpointName)`: opt-in; parses the named endpoint's `schema` (OpenAPI for HTTP, protobuf for gRPC) into a CEL optional wrapping a list of `{kind, service, method, path}` routes, for rendering exact per-route gateway matches. Consume it with `.orValue([])` or `.hasValue()`/`.value()`.
199+
200+
See [Helper Functions - Workload Helpers](./helper-functions.md#workload-helpers) for details.
201+
195202
### configurations
196203

197204
Configuration and secret references extracted from the workload container.

docs/reference/cel/helper-functions.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,73 @@ spec:
603603
- **BasePath usage**: For HTTPRoute path rewriting, use `workload.endpoints[endpointName].basePath` to configure URL path prefixes
604604
- **TargetPort distinction**: `targetPort` (container listening port) vs `port` (service port) - the helper uses the correct values for each
605605

606+
### workload.toEndpointResources(endpointName)
607+
608+
Parses a single endpoint's API schema (the OpenAPI document for HTTP endpoints, the protobuf definition for gRPC endpoints) and returns the flat list of routes it declares. This lets a ComponentType render **exact** per-route gateway matches (one match per OpenAPI path/method, or per gRPC service/method) instead of a single catch-all rule.
609+
610+
This helper is **opt-in**: endpoint schemas are only parsed when a template actually calls `workload.toEndpointResources(...)`, so templates that don't use it pay no parsing cost.
611+
612+
**Parameters:**
613+
614+
| Parameter | Type | Description |
615+
| -------------- | ------ | ------------------------------------------------------------------------------------ |
616+
| `endpointName` | string | The endpoint map key (e.g. `"http"`, `"grpc"`), the key used in `workload.endpoints` |
617+
618+
**Returns:** a CEL **optional** wrapping a list of route objects. Consume it with `.orValue([])`, or guard with `.hasValue()` / `.value()`. Each route object contains:
619+
620+
| Field | Type | Description |
621+
| --------- | ------ | --------------------------------------------------------------------------------------- |
622+
| `kind` | string | Protocol of the route: `"HTTP"` or `"gRPC"` |
623+
| `service` | string | Fully-qualified gRPC service name (e.g. `greeter.Greeter`). Empty for HTTP routes |
624+
| `method` | string | HTTP verb (`GET`/`POST`/...) for HTTP routes, or the gRPC method name (e.g. `SayHello`) |
625+
| `path` | string | HTTP path template (e.g. `/v1/pets/{id}`). Empty for gRPC routes |
626+
627+
Routes are returned in a deterministic (sorted) order so rendered output is stable.
628+
629+
:::note Best-effort extraction
630+
A missing, empty, or unparseable schema (or an endpoint protocol with no extractor, such as TCP/UDP) yields an empty list, never a render failure. Schema format is inferred from the endpoint type (`HTTP` → OpenAPI, `gRPC` → protobuf) unless `workload.endpoints[name].schema.type` overrides it. Only OpenAPI and protobuf extractors exist today; GraphQL and AsyncAPI are reserved and currently degrade to an empty list.
631+
:::
632+
633+
**Examples:**
634+
635+
```yaml
636+
# gRPC: render one GRPCRoute match per (service, method) from the proto schema.
637+
# Falls back to a catch-all rule (no matches) when the endpoint has no parseable schema.
638+
rules:
639+
- matches: >-
640+
${workload.toEndpointResources(endpoint.key).hasValue()
641+
? workload.toEndpointResources(endpoint.key).value().map(r,
642+
{"method": {"type": "Exact", "service": r.service, "method": r.method}})
643+
: oc_omit()}
644+
backendRefs:
645+
- name: ${metadata.componentName}
646+
port: ${endpoint.value.port}
647+
```
648+
649+
```yaml
650+
# HTTP: render one HTTPRoute rule per (path, method) from the OpenAPI schema.
651+
# Parameterized paths (/books/{id}) become a RegularExpression match; static paths use Exact.
652+
rules: >-
653+
${workload.toEndpointResources(endpoint.key).orValue([]).map(r, {
654+
"matches": [{
655+
"path": {
656+
"type": r.path.contains("{") ? "RegularExpression" : "Exact",
657+
"value": r.path.contains("{")
658+
? r.path.split("/").map(s, s.startsWith("{") ? "[^/]+" : s).join("/")
659+
: r.path
660+
},
661+
"method": r.method
662+
}],
663+
"backendRefs": [{"name": metadata.componentName, "port": endpoint.value.port}]
664+
})}
665+
```
666+
667+
**Notes:**
668+
669+
- Returns a CEL optional, not a plain list, so always unwrap with `.orValue([])` or `.hasValue()`/`.value()`.
670+
- Reads from `workload.endpoints[endpointName].schema`; an endpoint with no schema yields an empty list.
671+
- Complete, runnable ComponentType examples ship in the OpenChoreo repo under `samples/component-types/component-grpc-service/` and `samples/component-types/component-http-openapi-service/`.
672+
606673
## Common Usage Patterns
607674

608675
### Complete Deployment with Configurations

versioned_docs/version-v1.2.0-m.1/platform-engineer-guide/component-types/patching-syntax.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Traits can modify existing resources using patches, which are JSON Patch operati
1515
- CEL-based resource targeting
1616
- forEach iteration support
1717

18+
A Trait can also **delete** whole resources produced by the ComponentType or earlier traits using the `spec.removes` section. See [Removing Resources](#removing-resources).
19+
1820
## Basic Patch Structure
1921

2022
Patches are defined in the Trait's `spec.patches` section:
@@ -272,6 +274,73 @@ patches:
272274
name: ${port.name}
273275
```
274276

277+
## Removing Resources
278+
279+
Patches modify resources in place. When a Trait needs to **delete** a whole resource produced by the ComponentType or by an earlier trait, use the `spec.removes` section instead. Each entry matches resources by GVK (with an optional `where` filter) and removes the matched resources entirely from the rendered output.
280+
281+
```yaml
282+
apiVersion: openchoreo.dev/v1alpha1
283+
kind: Trait
284+
metadata:
285+
name: drop-default-route
286+
spec:
287+
removes:
288+
- target:
289+
kind: HTTPRoute
290+
group: gateway.networking.k8s.io
291+
version: v1
292+
targetPlane: dataplane
293+
```
294+
295+
A remove entry uses the same `target` shape as a patch, but has no `operations`. The whole matched resource is deleted:
296+
297+
| Field | Required | Description |
298+
| ----------------- | -------- | ---------------------------------------------------------------------- |
299+
| `target.kind` | Yes | Resource kind to remove (e.g. `HTTPRoute`, `ConfigMap`) |
300+
| `target.group` | Yes | API group; use `""` for core API resources |
301+
| `target.version` | Yes | API version (e.g. `v1`) |
302+
| `target.where` | No | CEL expression filtering which matching resources are removed |
303+
| `targetPlane` | No | Plane whose resources are targeted; defaults to `"dataplane"` |
304+
| `forEach` / `var` | No | Iterate over a CEL list, binding each item to `var` for use in `where` |
305+
306+
**Execution order** - Within a single trait, removes run **after** its `creates` and `patches`. This lets one trait fully express a substitution: create a replacement resource and then remove the original.
307+
308+
```yaml
309+
spec:
310+
creates:
311+
- template:
312+
apiVersion: v1
313+
kind: ConfigMap
314+
metadata:
315+
name: ${metadata.name}-tuned-config
316+
data:
317+
mode: optimized
318+
removes:
319+
# Drop the ConfigMap the ComponentType emitted, now that the tuned one exists
320+
- target:
321+
kind: ConfigMap
322+
group: ""
323+
version: v1
324+
where: ${resource.metadata.name == metadata.name + "-default-config"}
325+
```
326+
327+
:::warning Workload resources cannot be removed
328+
The primary workload is defined by the ComponentType, so traits **must not** delete it. The admission webhook rejects removes that target a built-in workload GVK: kinds `Deployment`, `StatefulSet`, `DaemonSet`, `CronJob`, or `Job` in the `apps` or `batch` groups. The match is on the full GVK, so a custom CRD that merely shares one of these kind names in a different group (e.g. `group: example.com`, `kind: Deployment`) is **not** rejected.
329+
:::
330+
331+
`forEach` is supported just like in patches, letting you remove a set of resources derived from a CEL list:
332+
333+
```yaml
334+
removes:
335+
- target:
336+
kind: HTTPRoute
337+
group: gateway.networking.k8s.io
338+
version: v1
339+
where: ${resource.metadata.labels["openchoreo.dev/endpoint-name"] == route}
340+
forEach: ${parameters.routesToDrop}
341+
var: route
342+
```
343+
275344
## Path Resolution Behavior
276345

277346
| Path Type | Operation | Behavior |

versioned_docs/version-v1.2.0-m.1/reference/cel/context-variables.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,13 @@ containers:
192192
port: ${workload.endpoints[endpoint].port}
193193
```
194194

195+
**Helper methods:** The `workload` object exposes two endpoint helpers:
196+
197+
- `workload.toServicePorts()`: converts the endpoints map into Kubernetes Service ports.
198+
- `workload.toEndpointResources(endpointName)`: opt-in; parses the named endpoint's `schema` (OpenAPI for HTTP, protobuf for gRPC) into a CEL optional wrapping a list of `{kind, service, method, path}` routes, for rendering exact per-route gateway matches. Consume it with `.orValue([])` or `.hasValue()`/`.value()`.
199+
200+
See [Helper Functions - Workload Helpers](./helper-functions.md#workload-helpers) for details.
201+
195202
### configurations
196203

197204
Configuration and secret references extracted from the workload container.

versioned_docs/version-v1.2.0-m.1/reference/cel/helper-functions.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,73 @@ spec:
603603
- **BasePath usage**: For HTTPRoute path rewriting, use `workload.endpoints[endpointName].basePath` to configure URL path prefixes
604604
- **TargetPort distinction**: `targetPort` (container listening port) vs `port` (service port) - the helper uses the correct values for each
605605

606+
### workload.toEndpointResources(endpointName)
607+
608+
Parses a single endpoint's API schema (the OpenAPI document for HTTP endpoints, the protobuf definition for gRPC endpoints) and returns the flat list of routes it declares. This lets a ComponentType render **exact** per-route gateway matches (one match per OpenAPI path/method, or per gRPC service/method) instead of a single catch-all rule.
609+
610+
This helper is **opt-in**: endpoint schemas are only parsed when a template actually calls `workload.toEndpointResources(...)`, so templates that don't use it pay no parsing cost.
611+
612+
**Parameters:**
613+
614+
| Parameter | Type | Description |
615+
| -------------- | ------ | ------------------------------------------------------------------------------------ |
616+
| `endpointName` | string | The endpoint map key (e.g. `"http"`, `"grpc"`), the key used in `workload.endpoints` |
617+
618+
**Returns:** a CEL **optional** wrapping a list of route objects. Consume it with `.orValue([])`, or guard with `.hasValue()` / `.value()`. Each route object contains:
619+
620+
| Field | Type | Description |
621+
| --------- | ------ | --------------------------------------------------------------------------------------- |
622+
| `kind` | string | Protocol of the route: `"HTTP"` or `"gRPC"` |
623+
| `service` | string | Fully-qualified gRPC service name (e.g. `greeter.Greeter`). Empty for HTTP routes |
624+
| `method` | string | HTTP verb (`GET`/`POST`/...) for HTTP routes, or the gRPC method name (e.g. `SayHello`) |
625+
| `path` | string | HTTP path template (e.g. `/v1/pets/{id}`). Empty for gRPC routes |
626+
627+
Routes are returned in a deterministic (sorted) order so rendered output is stable.
628+
629+
:::note Best-effort extraction
630+
A missing, empty, or unparseable schema (or an endpoint protocol with no extractor, such as TCP/UDP) yields an empty list, never a render failure. Schema format is inferred from the endpoint type (`HTTP` → OpenAPI, `gRPC` → protobuf) unless `workload.endpoints[name].schema.type` overrides it. Only OpenAPI and protobuf extractors exist today; GraphQL and AsyncAPI are reserved and currently degrade to an empty list.
631+
:::
632+
633+
**Examples:**
634+
635+
```yaml
636+
# gRPC: render one GRPCRoute match per (service, method) from the proto schema.
637+
# Falls back to a catch-all rule (no matches) when the endpoint has no parseable schema.
638+
rules:
639+
- matches: >-
640+
${workload.toEndpointResources(endpoint.key).hasValue()
641+
? workload.toEndpointResources(endpoint.key).value().map(r,
642+
{"method": {"type": "Exact", "service": r.service, "method": r.method}})
643+
: oc_omit()}
644+
backendRefs:
645+
- name: ${metadata.componentName}
646+
port: ${endpoint.value.port}
647+
```
648+
649+
```yaml
650+
# HTTP: render one HTTPRoute rule per (path, method) from the OpenAPI schema.
651+
# Parameterized paths (/books/{id}) become a RegularExpression match; static paths use Exact.
652+
rules: >-
653+
${workload.toEndpointResources(endpoint.key).orValue([]).map(r, {
654+
"matches": [{
655+
"path": {
656+
"type": r.path.contains("{") ? "RegularExpression" : "Exact",
657+
"value": r.path.contains("{")
658+
? r.path.split("/").map(s, s.startsWith("{") ? "[^/]+" : s).join("/")
659+
: r.path
660+
},
661+
"method": r.method
662+
}],
663+
"backendRefs": [{"name": metadata.componentName, "port": endpoint.value.port}]
664+
})}
665+
```
666+
667+
**Notes:**
668+
669+
- Returns a CEL optional, not a plain list, so always unwrap with `.orValue([])` or `.hasValue()`/`.value()`.
670+
- Reads from `workload.endpoints[endpointName].schema`; an endpoint with no schema yields an empty list.
671+
- Complete, runnable ComponentType examples ship in the OpenChoreo repo under `samples/component-types/component-grpc-service/` and `samples/component-types/component-http-openapi-service/`.
672+
606673
## Common Usage Patterns
607674

608675
### Complete Deployment with Configurations

0 commit comments

Comments
 (0)