Skip to content

Commit ff5d781

Browse files
stubbiclaude
andauthored
feat: optional Gateway API HTTPRoute for HermesInstance (#59)
## Summary Adds optional Gateway API HTTPRoute support to `HermesInstance`, mirroring paperclip-operator and the existing Ingress spec shape for consistency. This is a Tier 3 cross-pollination change built on `feat/parity-tier12`. - New `spec.networking.httpRoute` (`*HTTPRouteSpec`): `enabled`, `parentRefs` (`name`/`namespace`/`sectionName`), `hostnames`, `path`, `servicePortName`, `annotations`. Defaults off. - New `internal/resources/httproute.go` `BuildHTTPRoute`: builds an **unstructured** `gateway.networking.k8s.io/v1` HTTPRoute (no `sigs.k8s.io/gateway-api` dependency, following the `BuildServiceMonitor` pattern). A single `PathPrefix` rule routes to the agent Service; the named service port is resolved to its port number for the backendRef. - Controller: `reconcileHTTPRoute` step using `controllerutil.CreateOrUpdate` over an unstructured object, deleting the route when disabled, reporting via a new `HTTPRouteReady` condition. - Tolerates clusters without the Gateway API CRDs: when `httpRoute` is unset (default), the disabled path swallows the "no matches for kind" error so users are not forced to install Gateway API. Enabling `httpRoute` without the CRDs surfaces the error via `HTTPRouteReady=False`. - `+kubebuilder:rbac` for `gateway.networking.k8s.io/httproutes`; regenerated `config/rbac/role.yaml` and kept the Helm chart ClusterRole in sync (Helm RBAC Sync check passes). - Regenerated CRDs (config + Helm chart copy), deepcopy, and generated API reference. - Unit tests for the pure builder; docs in `api-reference.md` and `conditions.md`. Note: `bundle/` and `config/webhook` are intentionally untouched (a concurrent OLM webhook fix owns those off `main`); `sync-bundle-crds` was therefore not run here. ## Validation - `go build ./...`, `go vet ./...`: clean - `make lint` (golangci-lint v2.12.2): 0 issues - `go test ./internal/resources/... ./api/...`: pass (resources coverage 90.9%) - `make test` (envtest): controller suite passes (27/27, ~28s) after the Gateway-API-CRD tolerance fix - `hack/check-helm-rbac.sh`, `helm lint`, `hack/reconcile-guard.sh`: pass - dash scan `grep -rnP '[\x{2013}\x{2014}]'` on changed files: clean DO NOT MERGE / no auto-merge per task instructions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c81dfd6 commit ff5d781

13 files changed

Lines changed: 629 additions & 15 deletions

api/v1/hermesinstance_types.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,63 @@ type NetworkingSpec struct {
449449
// Ingress controls optional Ingress creation.
450450
// +optional
451451
Ingress IngressSpec `json:"ingress,omitempty"`
452+
453+
// HTTPRoute controls optional Gateway API HTTPRoute creation. The operator
454+
// emits an unstructured gateway.networking.k8s.io/v1 HTTPRoute; the Gateway
455+
// API CRDs must be installed in the cluster for this to take effect.
456+
// +optional
457+
HTTPRoute *HTTPRouteSpec `json:"httpRoute,omitempty"`
458+
}
459+
460+
// HTTPRouteSpec controls optional Gateway API HTTPRoute creation. It mirrors the
461+
// IngressSpec shape for consistency: a single prefix rule routing to the agent
462+
// Service. The route is only created when Enabled is true.
463+
type HTTPRouteSpec struct {
464+
// Enabled: when true, the operator creates an HTTPRoute for the agent.
465+
// Default false.
466+
// +kubebuilder:default=false
467+
// +optional
468+
Enabled *bool `json:"enabled,omitempty"`
469+
470+
// ParentRefs are the Gateways (or other parents) this route attaches to.
471+
// At least one is required for the route to take effect.
472+
// +optional
473+
ParentRefs []HTTPRouteParentRef `json:"parentRefs,omitempty"`
474+
475+
// Hostnames are the hostnames matched by this route.
476+
// +listType=set
477+
// +optional
478+
Hostnames []string `json:"hostnames,omitempty"`
479+
480+
// Path is the path prefix routed to the agent Service. Default "/".
481+
// +kubebuilder:default="/"
482+
// +optional
483+
Path string `json:"path,omitempty"`
484+
485+
// ServicePortName: name of the Service port the route should target.
486+
// Default "gateway".
487+
// +kubebuilder:default="gateway"
488+
// +optional
489+
ServicePortName string `json:"servicePortName,omitempty"`
490+
491+
// Annotations are applied verbatim onto the HTTPRoute.
492+
// +optional
493+
Annotations map[string]string `json:"annotations,omitempty"`
494+
}
495+
496+
// HTTPRouteParentRef references a parent (typically a Gateway) the route attaches to.
497+
type HTTPRouteParentRef struct {
498+
// Name of the parent resource (e.g. the Gateway name).
499+
// +kubebuilder:validation:MinLength=1
500+
Name string `json:"name"`
501+
502+
// Namespace of the parent. Defaults to the HermesInstance namespace when empty.
503+
// +optional
504+
Namespace *string `json:"namespace,omitempty"`
505+
506+
// SectionName is the name of a section within the parent (e.g. a Gateway listener).
507+
// +optional
508+
SectionName *string `json:"sectionName,omitempty"`
452509
}
453510

454511
// ServiceSpec controls the agent's Service.
@@ -1057,6 +1114,7 @@ const (
10571114
ConditionTypePDBReady = "PDBReady"
10581115
ConditionTypeHPAReady = "HPAReady"
10591116
ConditionTypeIngressReady = "IngressReady"
1117+
ConditionTypeHTTPRouteReady = "HTTPRouteReady"
10601118
ConditionTypeServiceMonitorReady = "ServiceMonitorReady"
10611119
ConditionTypePrometheusRuleReady = "PrometheusRuleReady"
10621120
ConditionTypeGrafanaDashboardReady = "GrafanaDashboardReady"

api/v1/zz_generated.deepcopy.go

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bundle/manifests/hermes-operator.clusterserviceversion.yaml

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ metadata:
1111
support: paperclipinc
1212
repository: https://github.com/paperclipinc/hermes-operator
1313
description: >-
14-
A production-grade Kubernetes operator for deploying and managing
15-
nousresearch/hermes-agent: a Python-based self-improving multi-platform AI
16-
agent.
14+
A production-grade Kubernetes operator for deploying and managing nousresearch/hermes-agent: a Python-based self-improving multi-platform AI agent.
1715
alm-examples: |-
1816
[
1917
{
@@ -119,8 +117,7 @@ spec:
119117
120118
WhatsApp, Signal), self-improves through a built-in learning loop, persists
121119
122-
session memory via FTS5, models users via Honcho dialectic profiles, and
123-
runs
120+
session memory via FTS5, models users via Honcho dialectic profiles, and runs
124121
125122
scheduled automations via a native cron scheduler.
126123
@@ -129,23 +126,31 @@ spec:
129126
130127
131128
- **Declarative Deployment**: every aspect of an agent's runtime, gateways,
129+
132130
storage, networking, observability, and security through a single
133131
`HermesInstance` resource.
134132
- **Self-Configuration with Audit**: the agent persists learned skills, env
133+
135134
vars, config patches, and workspace files via `HermesSelfConfig` CRs that
136135
the operator validates against an explicit allowlist, then applies via
137136
Server-Side Apply (no GitOps flap).
138137
- **Cluster Defaults**: `HermesClusterDefaults` (cluster-scoped singleton)
138+
139139
supplies organization-wide defaults for image, storage class, IRSA, etc.
140140
- **Auto-Update with Rollback**: OCI registry polling with pre-update backup
141+
141142
and probe-driven rollback.
142143
- **Backup / Restore**: S3-compatible (R2, MinIO, AWS), backup-on-delete
144+
143145
finalizer, declarative restore from snapshot.
144146
- **OpenClaw Migration**: one-shot `spec.migration.fromOpenClaw` invokes
147+
145148
hermes-agent's built-in importer.
146149
- **Honcho Profile Store**: optional companion Deployment with persistence
150+
147151
for dialectic user profiles.
148152
- **Multi-Platform Gateways**: first-class config for Telegram, Discord,
153+
149154
Slack, WhatsApp, Signal: each with isolated secret rotation.
150155
151156
### Default Security Posture
@@ -190,12 +195,14 @@ spec:
190195
3. Create a `HermesInstance` referencing the Secret.
191196
192197
4. The operator handles the rest: deployment, networking, security,
198+
193199
lifecycle.
194200
195201
### Prerequisites
196202
197203
198204
- Kubernetes 1.28+
205+
199206
maturity: alpha
200207
version: 0.1.11
201208
minKubeVersion: 1.28.0
@@ -251,8 +258,7 @@ spec:
251258
kind: HermesInstance
252259
displayName: Hermes Instance
253260
description: >-
254-
Represents a managed hermes-agent instance with security, networking,
255-
gateways, storage, and observability.
261+
Represents a managed hermes-agent instance with security, networking, gateways, storage, and observability.
256262
specDescriptors:
257263
- path: image.repository
258264
displayName: Image Repository
@@ -387,8 +393,7 @@ spec:
387393
kind: HermesSelfConfig
388394
displayName: Hermes SelfConfig
389395
description: >-
390-
Agent-initiated mutation request, validated against the parent
391-
instance's selfConfigure policy and applied via Server-Side Apply.
396+
Agent-initiated mutation request, validated against the parent instance's selfConfigure policy and applied via Server-Side Apply.
392397
specDescriptors:
393398
- path: instanceRef
394399
displayName: Instance Reference
@@ -420,8 +425,7 @@ spec:
420425
kind: HermesClusterDefaults
421426
displayName: Hermes Cluster Defaults
422427
description: >-
423-
Cluster-scoped singleton that fills nil fields on every
424-
HermesInstance. Name must be 'cluster'.
428+
Cluster-scoped singleton that fills nil fields on every HermesInstance. Name must be 'cluster'.
425429
specDescriptors:
426430
- path: image.repository
427431
displayName: Default Image Repository
@@ -443,7 +447,7 @@ spec:
443447
- serviceAccountName: hermes-operator-controller-manager
444448
rules:
445449
- apiGroups:
446-
- ''
450+
- ""
447451
resources:
448452
- configmaps
449453
- events
@@ -460,7 +464,7 @@ spec:
460464
- update
461465
- watch
462466
- apiGroups:
463-
- ''
467+
- ""
464468
resources:
465469
- pods
466470
verbs:
@@ -505,6 +509,18 @@ spec:
505509
- patch
506510
- update
507511
- watch
512+
- apiGroups:
513+
- gateway.networking.k8s.io
514+
resources:
515+
- httproutes
516+
verbs:
517+
- create
518+
- delete
519+
- get
520+
- list
521+
- patch
522+
- update
523+
- watch
508524
- apiGroups:
509525
- hermes.agent
510526
resources:

charts/hermes-operator/templates/clusterrole.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ rules:
2020
- apiGroups: [batch]
2121
resources: [cronjobs, jobs]
2222
verbs: [create, delete, get, list, patch, update, watch]
23+
- apiGroups: [gateway.networking.k8s.io]
24+
resources: [httproutes]
25+
verbs: [create, delete, get, list, patch, update, watch]
2326
- apiGroups: [hermes.agent]
2427
resources: [hermesclusterdefaults]
2528
verbs: [get, list, patch, update, watch]

charts/hermes-operator/templates/crds/hermes.agent_hermesinstances.yaml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4641,6 +4641,66 @@ spec:
46414641
networking:
46424642
description: Networking exposes the agent via Service / Ingress.
46434643
properties:
4644+
httpRoute:
4645+
description: |-
4646+
HTTPRoute controls optional Gateway API HTTPRoute creation. The operator
4647+
emits an unstructured gateway.networking.k8s.io/v1 HTTPRoute; the Gateway
4648+
API CRDs must be installed in the cluster for this to take effect.
4649+
properties:
4650+
annotations:
4651+
additionalProperties:
4652+
type: string
4653+
description: Annotations are applied verbatim onto the HTTPRoute.
4654+
type: object
4655+
enabled:
4656+
default: false
4657+
description: |-
4658+
Enabled: when true, the operator creates an HTTPRoute for the agent.
4659+
Default false.
4660+
type: boolean
4661+
hostnames:
4662+
description: Hostnames are the hostnames matched by this route.
4663+
items:
4664+
type: string
4665+
type: array
4666+
x-kubernetes-list-type: set
4667+
parentRefs:
4668+
description: |-
4669+
ParentRefs are the Gateways (or other parents) this route attaches to.
4670+
At least one is required for the route to take effect.
4671+
items:
4672+
description: HTTPRouteParentRef references a parent (typically
4673+
a Gateway) the route attaches to.
4674+
properties:
4675+
name:
4676+
description: Name of the parent resource (e.g. the Gateway
4677+
name).
4678+
minLength: 1
4679+
type: string
4680+
namespace:
4681+
description: Namespace of the parent. Defaults to the
4682+
HermesInstance namespace when empty.
4683+
type: string
4684+
sectionName:
4685+
description: SectionName is the name of a section within
4686+
the parent (e.g. a Gateway listener).
4687+
type: string
4688+
required:
4689+
- name
4690+
type: object
4691+
type: array
4692+
path:
4693+
default: /
4694+
description: Path is the path prefix routed to the agent Service.
4695+
Default "/".
4696+
type: string
4697+
servicePortName:
4698+
default: gateway
4699+
description: |-
4700+
ServicePortName: name of the Service port the route should target.
4701+
Default "gateway".
4702+
type: string
4703+
type: object
46444704
ingress:
46454705
description: Ingress controls optional Ingress creation.
46464706
properties:

0 commit comments

Comments
 (0)