Skip to content

feat: add Gateway API exposure for CloudShell - #714

Open
panpan0000 wants to merge 2 commits into
mainfrom
codex/gateway-api-exposure
Open

panpan0000 wants to merge 2 commits into
mainfrom
codex/gateway-api-exposure

Conversation

@panpan0000

@panpan0000 panpan0000 commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Add GatewayAPI as a CloudShell exposure mode.
  • Configure one administrator-owned Gateway and listener through Helm/operator flags.
  • Create one namespaced HTTPRoute per CloudShell, with an owner reference, path rewrite, and worker Service backend.
  • Publish status.accessUrl only after the configured Gateway reports Accepted=True and ResolvedRefs=True for the current route generation.
  • Explicitly delete the HTTPRoute when a CloudShell reaches TTL or is deleted, including the cleanup: false case.
  • Vendor the Gateway API dependency so operator image builds do not depend on downloading Go modules during Docker builds.

User-facing configuration

Install/configure the operator with an existing Gateway:

helm install cloudtty-operator cloudtty/cloudtty \
  --set gatewayAPI.gatewayName=cloudtty-gateway \
  --set gatewayAPI.gatewayNamespace=gateway-system \
  --set gatewayAPI.sectionName=http

Users then create a CloudShell with spec.exposureMode: GatewayAPI. The operator creates the HTTPRoute in the CloudShell namespace; the Gateway administrator controls namespace admission through allowedRoutes.

Functional validation

Environment: OrbStack Kubernetes v1.34.8, an agentgateway GatewayClass, and an isolated HTTP Gateway with allowedRoutes.namespaces.from: All.

Code/build checks:

  • CGO_ENABLED=0 GOFLAGS=-mod=vendor go test $(go list ./... | rg -v '/test/e2e$') — passed.
  • CGO_ENABLED=0 GOFLAGS=-mod=vendor go vet ./... — passed.
  • helm lint charts/cloudtty — passed.
  • Helm template rendering with all Gateway API values — passed.
  • docker --context orbstack build -f docker/operator/Dockerfile -t cloudtty-gatewayapi:final . — passed.

Core end-to-end records:

$ kubectl get gatewayclass agentgateway -o jsonpath='{.metadata.name} {.spec.controllerName} {.status.conditions[?(@.type=="Accepted")].status}'
agentgateway agentgateway.dev/agentgateway True

$ kubectl apply -f charts/cloudtty/_crds/cloudshell.cloudtty.io_cloudshells.yaml
customresourcedefinition.apiextensions.k8s.io/cloudshells.cloudshell.cloudtty.io created

$ kubectl apply -f gateway.yaml
gateway.gateway.networking.k8s.io/cloudtty-gateway created

$ kubectl get gateway cloudtty-gateway -n cloudtty-gateway-e2e
cloudtty-gateway-e2e/cloudtty-gateway class=agentgateway programmed=True
apiVersion: cloudshell.cloudtty.io/v1alpha1
kind: CloudShell
metadata:
  name: gateway-record
  namespace: cloudtty-gateway-user
spec:
  commandAction: bash
  exposureMode: GatewayAPI
  cleanup: false
$ kubectl apply -f gateway-record.yaml
cloudshell.cloudshell.cloudtty.io/gateway-record created

$ kubectl get cloudshell gateway-record -n cloudtty-gateway-user
name=gateway-record namespace=cloudtty-gateway-user exposureMode=GatewayAPI phase= accessUrl=

poll=1 phase= accessUrl=<empty> condition=<none> route=<none>
poll=2 phase= accessUrl=<empty> condition=<none> route=<none>
poll=3 phase=Ready accessUrl=/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-record condition=cloudtty.io/GatewayRouteReady=True:Accepted route=httproute.gateway.networking.k8s.io/cloudshell-gateway-record

Verify the generated HTTPRoute

$ kubectl get httproute cloudshell-gateway-record -n cloudtty-gateway-user
owner=CloudShell/gateway-record
parent=cloudtty-gateway-e2e/cloudtty-gateway section=http
path=/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-record
rewrite=/
backend=cloudshell-worker-rplkgskvw8:7681
routeStatus=Accepted=True:Accepted:observedGeneration=1,ResolvedRefs=True:ResolvedRefs:observedGeneration=1

The route was accessed through the Gateway Service:

$ kubectl get svc cloudtty-gateway -n cloudtty-gateway-e2e
gateway_ip=192.168.139.2 access_url=/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-record

$ curl http://192.168.139.2:8080/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-record
http_status=200
body=cloudtty gateway api e2e

Gateway admission rejection and recovery
Change the Gateway listener from All to Same:

$ kubectl patch gateway cloudtty-gateway -n cloudtty-gateway-e2e ...
gateway.gateway.networking.k8s.io/cloudtty-gateway patched

reject_poll=1 accessUrl=/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-record cloudshellCondition=True:Accepted routeStatus=Accepted=False:NotAllowedByListeners,ResolvedRefs=True:ResolvedRefs
reject_poll=2 accessUrl=<empty> cloudshellCondition=False:NotAllowedByListeners routeStatus=Accepted=False:NotAllowedByListeners,ResolvedRefs=True:ResolvedRefs
allowedRoutes=Same

Restore allowedRoutes.from: All:

$ kubectl patch gateway cloudtty-gateway -n cloudtty-gateway-e2e ...
gateway.gateway.networking.k8s.io/cloudtty-gateway patched

recover_poll=1 accessUrl=<empty> cloudshellCondition=False:NotAllowedByListeners routeStatus=Accepted=True:Accepted,ResolvedRefs=True:ResolvedRefs
recover_poll=2 accessUrl=/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-record cloudshellCondition=True:Accepted routeStatus=Accepted=True:Accepted,ResolvedRefs=True:ResolvedRefs
recovered_http_status=200
recovered_body=cloudtty gateway api e2e

delete it

$ kubectl delete cloudshell gateway-record -n cloudtty-gateway-user
cloudshell.cloudshell.cloudtty.io "gateway-record" deleted from cloudtty-gateway-user namespace

delete_poll=1 cloudshell=<deleted> route=<deleted>

TTL cleanup with cleanup=false

apiVersion: cloudshell.cloudtty.io/v1alpha1
kind: CloudShell
metadata:
  name: gateway-ttl-record
  namespace: cloudtty-gateway-user
spec:
  commandAction: bash
  exposureMode: GatewayAPI
  cleanup: false
  ttlSecondsAfterStarted: 5
$ kubectl apply -f gateway-ttl-record.yaml
cloudshell.cloudshell.cloudtty.io/gateway-ttl-record created

ttl_poll=1 phase=<empty> accessUrl=<empty> route=httproute.gateway.networking.k8s.io/cloudshell-gateway-ttl-record condition=<none>
ttl_poll=2 phase=Ready accessUrl=/apis/v1alpha1/cloudshell/cloudtty-gateway-user/gateway-ttl-record?ttl=5 route=httproute.gateway.networking.k8s.io/cloudshell-gateway-ttl-record condition=True:Accepted
ttl_poll=5 phase=Complete accessUrl=<empty> route=<deleted> condition=False:RouteRemoved

final_name=gateway-ttl-record
final_phase=Complete
final_accessUrl=
final_condition=cloudtty.io/GatewayRouteReady=False:RouteRemoved

The rejection/recovery test changed the Gateway listener admission policy from All to Same and restored it afterward. The TTL test used cleanup: false, proving that the CloudShell CR can remain while its HTTPRoute and URL are removed.

@github-actions

Copy link
Copy Markdown

Hi @panpan0000,
Thanks for your pull request!
If the PR is ready, use the /auto-cc command to assign Reviewer to Review.
We will review it shortly.

Details

Instructions for interacting with me using comments are available here.
If you have questions or suggestions related to my behavior, please file an issue against the gh-ci-bot repository.

@panpan0000

Copy link
Copy Markdown
Member Author

to address #707

This branch has not been deployed

No deployments
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