From 6e9c427713b7e288798751fad0e0b221cd2618d8 Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Mon, 28 Sep 2026 13:35:09 -0500
Subject: [PATCH 01/12] HYPERSHELL-354 Specs
---
specs/index.spec.md | 1 +
.../gateway-provision-placement.spec.md | 257 ++++++++++++++++++
2 files changed, 258 insertions(+)
create mode 100644 specs/web-console/gateway-provision-placement.spec.md
diff --git a/specs/index.spec.md b/specs/index.spec.md
index 2b83b77a4..3f3d38849 100644
--- a/specs/index.spec.md
+++ b/specs/index.spec.md
@@ -44,6 +44,7 @@ Machine-readable index for autonomous reconciliation (`/reconcile` skill).
| `platform/openshell-inference-routing.spec.md` | platform | Inference router, inference.local, credential-free sandbox model access, provider translation | CP | openshell-gateway, openshell-gateway-credentials |
| `platform/global-architecture.spec.md` | platform | Global hub, multi-cloud, cloud-managed PostgreSQL, Tekton, ArgoCD, Vault | CP, ALL | data-model, control-plane |
| `web-console/architecture.spec.md` | web-console | Web console, BFF, browser session, UI routes | WEB, SDK, API | data-model, security, openshell-gateway-service-accounts, UI standards |
+| `web-console/gateway-provision-placement.spec.md` | web-console | Gateway placement intent, network/provider selection, random matching-cluster resolution | WEB, API, CP | architecture, managed-cluster-registration, openshell-gateway, UI standards |
| `web-console/operational-dashboard.spec.md` | web-console | Widgetized operational dashboard, Prometheus-backed metrics adapter, admin access | WEB | web-console/architecture, gateway-metrics-dashboard, gateway-fleet-total-trend, gateway-sandbox-active-trends, hub-cluster-utilization-trends, platform-inventory, registered-users, UI standards |
| `web-console/tracing.spec.md` | web-console | Browser OTel trace sink, BFF W3C propagation, telemetry ingest, dev Jaeger | WEB, BFF | web-console/architecture, domain-observability, local-development |
| `platform/gateway-metrics-dashboard.spec.md` | platform | Prometheus gateway phase metric, BFF metrics proxy, GatewayMetricsDashboard, operational dashboard gateway counts | API, WEB, deploy | data-model, web-console/architecture, local-development |
diff --git a/specs/web-console/gateway-provision-placement.spec.md b/specs/web-console/gateway-provision-placement.spec.md
new file mode 100644
index 000000000..d574fc48f
--- /dev/null
+++ b/specs/web-console/gateway-provision-placement.spec.md
@@ -0,0 +1,257 @@
+# Gateway Provision Placement Specification
+
+## Purpose
+
+Define the placement choices presented while provisioning an OpenShell gateway and the server-side resolution of those choices to a concrete `ManagedCluster`. The provision form SHALL let a user choose exactly one network visibility—VPN or public. Public placement SHALL support AWS and IBM Cloud. VPN placement SHALL support AWS only. The client SHALL submit placement intent rather than a cluster identifier. The backend SHALL randomly select a matching eligible cluster and persist its identifier on the created gateway.
+
+## Requirements
+
+### Requirement: GP-01 -- Placement Intent Contract
+
+The gateway provision request SHALL contain the gateway name and one of these placement intents:
+
+- `{ network: "vpn", provider: "aws" }`
+- `{ network: "public", provider: "aws" | "ibm" }`
+- `{ mode: "local-kind" }`
+
+The client SHALL NOT submit a user-selected `cluster_id`. The backend SHALL resolve and assign the concrete `cluster_id` before creating the gateway.
+
+#### Scenario: Public IBM placement is submitted as intent
+
+- GIVEN the user enters a valid gateway name
+- AND selects Public
+- AND selects IBM Cloud
+- WHEN the user submits the form
+- THEN the request SHALL contain `network: public` and `provider: ibm`
+- AND the request SHALL NOT contain a concrete `cluster_id`
+
+#### Scenario: VPN placement is submitted as AWS intent
+
+- GIVEN the user enters a valid gateway name
+- AND selects VPN
+- WHEN the user submits the form
+- THEN the request SHALL contain `network: vpn` and `provider: aws`
+- AND the request SHALL NOT contain a concrete `cluster_id`
+
+### Requirement: GP-02 -- Network Visibility Selection
+
+The provision form SHALL present VPN and Public as mutually exclusive choices. The form SHALL require exactly one network visibility before submission. Selecting one visibility SHALL clear the other visibility.
+
+The form SHALL NOT present the former cluster selector.
+
+#### Scenario: Network visibility is required
+
+- GIVEN the gateway name is valid
+- AND neither VPN nor Public is selected
+- WHEN the user activates Provision gateway
+- THEN the form SHALL identify network visibility as required
+- AND no provision request SHALL be sent
+
+#### Scenario: VPN and Public cannot both be selected
+
+- GIVEN Public is selected
+- WHEN the user selects VPN
+- THEN VPN SHALL become selected
+- AND Public SHALL become unselected
+
+### Requirement: GP-03 -- Provider Selection
+
+The provision form SHALL present AWS and IBM Cloud as mutually exclusive choices. The form SHALL require exactly one provider before submission.
+
+IBM Cloud SHALL be selected by default when Public becomes active. When VPN becomes active, AWS SHALL be selected and IBM Cloud SHALL be disabled. The user SHALL NOT be able to submit VPN placement with IBM Cloud.
+
+#### Scenario: Public defaults to IBM Cloud
+
+- GIVEN the user selects Public
+- WHEN the provider controls render
+- THEN IBM Cloud SHALL be selected
+- AND AWS SHALL be unselected
+
+#### Scenario: VPN supports AWS only
+
+- GIVEN the user selects VPN
+- WHEN the provider controls render
+- THEN AWS SHALL be selected
+- AND IBM Cloud SHALL be disabled
+
+#### Scenario: Provider is required
+
+- GIVEN a network visibility is selected
+- AND neither AWS nor IBM Cloud is selected
+- WHEN the user activates Provision gateway
+- THEN the form SHALL identify provider as required
+- AND no provision request SHALL be sent
+
+### Requirement: GP-04 -- Placement Availability
+
+The form SHALL obtain placement availability from current managed-cluster data. Each managed cluster used for placement SHALL identify its provider and supported network visibility in backend-owned placement data. AWS SHALL support Public and VPN placement. IBM Cloud SHALL support Public placement only. For each supported network and provider pair, availability SHALL require at least one registered cluster with a connected control plane that matches both choices.
+
+The form SHALL disable a provider for the selected network when no eligible cluster matches that pair. When neither provider is available for a network, the form SHALL disable that network choice. Disabled controls SHALL communicate why the placement is unavailable.
+
+Placement availability SHALL NOT depend on CPU, memory, pod usage, or other capacity data.
+
+The form SHALL load placement availability automatically when it opens. The form SHALL refresh placement availability after a placement-related submission error. The form SHALL NOT require a separate manual refresh or Retry control.
+
+A placement pair that becomes unavailable before submission SHALL be rejected by the backend. The form SHALL present the rejection as a recoverable provisioning error.
+
+#### Scenario: AWS is unavailable for Public
+
+- GIVEN no eligible AWS cluster matches Public
+- AND an eligible IBM Cloud cluster matches Public
+- WHEN the user selects Public
+- THEN AWS SHALL be disabled
+- AND IBM Cloud SHALL remain selectable
+
+#### Scenario: IBM Cloud is unsupported for VPN
+
+- GIVEN the user selects VPN
+- WHEN the provider controls render
+- THEN IBM Cloud SHALL be disabled regardless of IBM Cloud cluster availability
+- AND the form SHALL communicate that IBM Cloud does not support VPN placement
+
+#### Scenario: Both managed networks are unavailable
+
+- GIVEN no eligible AWS or IBM Cloud cluster matches Public or VPN
+- WHEN the provision form renders
+- THEN Public and VPN SHALL be disabled
+- AND the form SHALL explain that no managed placement is currently available
+- AND the Provision gateway button SHALL remain enabled
+
+#### Scenario: Capacity data does not affect availability
+
+- GIVEN an eligible IBM Cloud cluster matches Public
+- AND capacity data for that cluster is unavailable
+- WHEN the provider controls render
+- THEN IBM Cloud SHALL remain selectable
+
+### Requirement: GP-05 -- Local-kind Development Placement
+
+When the platform is running in the Kind development environment and the registered `local-kind` cluster is connected, and no managed placement is available, the form SHALL expose a **Use local-kind** option. The option SHALL NOT be shown when an AWS or IBM Cloud managed placement is available, or outside the Kind development environment.
+
+Local-kind SHALL be mutually exclusive with VPN and Public. Local-kind SHALL be selected by default only when no managed placement is available.
+
+Selecting local-kind SHALL submit `{ placement: { mode: "local-kind" } }`.
+
+#### Scenario: Local-kind appears for Kind development
+
+- GIVEN the platform is running in the Kind development environment
+- AND the registered `local-kind` cluster is connected
+- WHEN the provision form renders
+- THEN Use local-kind SHALL be visible
+- AND Use local-kind SHALL be available for selection
+
+#### Scenario: Local-kind is hidden with managed placement
+
+- GIVEN a managed placement pair is available
+- AND the platform is running in the Kind development environment
+- AND the registered `local-kind` cluster is connected
+- WHEN the provision form renders
+- THEN Use local-kind SHALL NOT be visible
+- AND managed placement choices SHALL remain available
+
+#### Scenario: Local-kind is hidden outside Kind development
+
+- GIVEN no managed placement pair is available
+- AND the platform is not running in the Kind development environment
+- WHEN the provision form renders
+- THEN Use local-kind SHALL NOT be visible
+- AND the form SHALL explain that no managed placement is currently available
+
+### Requirement: GP-06 -- Form Submission Validation
+
+The Provision gateway button SHALL remain enabled regardless of whether the form is complete, valid, or displaying validation errors. Activating the button SHALL run form validation and show field-level errors for invalid or missing values. The form SHALL NOT send a provision request until validation succeeds.
+
+The existing gateway-name validation and normalization rules SHALL remain unchanged.
+
+#### Scenario: Invalid form does not submit
+
+- GIVEN the gateway name or placement choices are invalid
+- WHEN the user activates Provision gateway
+- THEN the corresponding field-level errors SHALL be shown
+- AND no provision request SHALL be sent
+
+#### Scenario: Valid form submits
+
+- GIVEN the gateway name and placement choices are valid
+- WHEN the user activates Provision gateway
+- THEN the placement-intent request SHALL be sent
+- AND the button MAY show a loading state while the request is pending
+
+### Requirement: GP-07 -- Backend Placement Resolution
+
+The backend SHALL reject a VPN IBM Cloud placement intent as unsupported. The backend SHALL resolve every supported managed placement intent to the set of registered clusters with connected control planes that match the requested network and provider.
+
+When one eligible cluster matches, the backend SHALL select that cluster. When multiple eligible clusters match, the backend SHALL select one at random. Each matching eligible cluster SHALL have an equal probability of selection.
+
+The backend SHALL NOT inspect, score, or rank clusters by CPU, memory, pod usage, or other capacity data. The client SHALL neither calculate nor submit the selected cluster.
+
+If no eligible cluster matches the requested network and provider, the backend SHALL return a placement-unavailable error. The backend SHALL NOT fall back to another network, another provider, or local-kind.
+
+The backend SHALL persist the selected cluster identifier on the Gateway record. Gateway reconciliation SHALL use the persisted identifier.
+
+#### Scenario: Backend randomly selects a matching cluster
+
+- GIVEN two eligible AWS clusters match a public AWS intent
+- WHEN the backend creates the gateway
+- THEN the backend SHALL randomly select one of the two clusters
+- AND the persisted Gateway record SHALL contain the selected cluster's identifier
+
+#### Scenario: Backend does not use capacity
+
+- GIVEN two eligible IBM Cloud clusters match a public IBM Cloud intent
+- AND one cluster has more available capacity than the other
+- WHEN the backend creates the gateway
+- THEN both clusters SHALL remain eligible for random selection
+
+#### Scenario: Backend re-evaluates stale availability
+
+- GIVEN the client submits a valid placement intent
+- AND the cluster available when the form rendered is no longer eligible
+- WHEN the backend resolves placement
+- THEN the backend SHALL select another eligible matching cluster
+- OR the backend SHALL return a placement-unavailable error
+
+### Requirement: GP-08 -- Local-kind Placement Resolution
+
+When local-kind intent is submitted, the backend SHALL resolve it to the registered `local-kind` `ManagedCluster` in the Kind development environment.
+
+The backend SHALL reject local-kind placement with a typed client-visible error when the platform is not running in the Kind development environment, `local-kind` is not registered, or `local-kind` is not connected.
+
+#### Scenario: Local-kind resolves to the local cluster
+
+- GIVEN the client submits a valid local-kind intent
+- AND the registered `local-kind` cluster is connected
+- WHEN the backend creates the gateway
+- THEN the gateway SHALL be assigned to `local-kind`
+
+### Requirement: GP-09 -- API and Persistence Compatibility
+
+The API contract, generated clients or adapters, authorization boundaries, and gateway provisioning tests SHALL be updated consistently for the placement-intent request. Existing gateway records and read APIs SHALL continue to expose their resolved `cluster_id`.
+
+The change SHALL NOT require a database migration unless the implementation introduces persisted placement-intent fields. Placement intent MAY remain request-scoped after the backend resolves `cluster_id`.
+
+#### Scenario: Existing gateway placement remains readable
+
+- GIVEN a gateway was created through the placement-intent flow
+- WHEN a gateway detail or list API is read
+- THEN the response SHALL expose the resolved cluster using the existing gateway placement representation
+
+### Requirement: GP-10 -- Accessibility and Localization
+
+The network, provider, availability, disabled-state, validation, local-kind, and error messages SHALL be localized. The controls SHALL expose accessible names and selected or disabled state through semantic PatternFly form controls. Keyboard users SHALL be able to select and change network, provider, and local-kind placement without pointer input.
+
+#### Scenario: Disabled provider is announced
+
+- GIVEN VPN is selected
+- WHEN a keyboard or assistive-technology user inspects the provider controls
+- THEN IBM Cloud SHALL be exposed as disabled
+- AND the unsupported reason SHALL be conveyed in localized text or an accessible description
+
+## Non-Goals
+
+- Allowing users to choose both VPN and Public.
+- Supporting IBM Cloud VPN placement.
+- Allowing users to select a concrete managed cluster from the provision form.
+- Capacity-aware cluster selection or placement scoring.
+- Changing existing gateway naming rules.
+- Changing the resolved `cluster_id` representation on existing gateway records.
From 0f28dc43a71778cd056a2f4b5ca845a74848f1c8 Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Tue, 29 Sep 2026 15:26:40 -0500
Subject: [PATCH 02/12] HYPERSHELL-354 Code
---
components/api-server/go.mod | 2 +
components/api-server/go.sum | 10 +-
.../api-server/openapi/openapi.gateways.yaml | 82 ++-
.../openapi/openapi.managedClusters.yaml | 16 +
components/api-server/openapi/openapi.yaml | 6 +
.../pkg/api/grpc/hypershell/v1/gateways.pb.go | 470 ++++++++++-----
.../pkg/api/openapi/.openapi-generator/FILES | 4 +
.../api-server/pkg/api/openapi/README.md | 3 +
.../pkg/api/openapi/api/openapi.yaml | 135 ++++-
.../api-server/pkg/api/openapi/api_default.go | 119 ++++
.../pkg/api/openapi/docs/DefaultAPI.md | 64 +-
.../api/openapi/docs/GatewayCreateRequest.md | 32 +-
.../docs/GatewayPlacementAvailability.md | 187 ++++++
.../openapi/docs/GatewayPlacementIntent.md | 108 ++++
.../pkg/api/openapi/docs/ManagedCluster.md | 28 +-
.../docs/ManagedClusterRegistrationRequest.md | 44 +-
.../openapi/model_gateway_create_request.go | 46 +-
.../model_gateway_placement_availability.go | 340 +++++++++++
.../openapi/model_gateway_placement_intent.go | 197 +++++++
.../pkg/api/openapi/model_managed_cluster.go | 60 +-
...el_managed_cluster_registration_request.go | 60 +-
.../gateways/availability_handler_test.go | 31 +
.../plugins/gateways/cluster_fixture_test.go | 16 +-
.../plugins/gateways/cluster_reference.go | 33 ++
.../plugins/gateways/grpc_handler.go | 42 +-
.../plugins/gateways/grpc_integration_test.go | 8 +-
.../api-server/plugins/gateways/handler.go | 37 +-
.../plugins/gateways/integration_test.go | 98 ++--
.../plugins/gateways/phase_validation_test.go | 12 +-
.../api-server/plugins/gateways/placement.go | 84 +++
.../plugins/gateways/placement_test.go | 90 +++
.../api-server/plugins/gateways/plugin.go | 136 ++++-
.../api-server/plugins/gateways/presenter.go | 3 +-
.../api-server/plugins/managedClusters/dao.go | 14 +-
.../plugins/managedClusters/handler.go | 14 +-
.../plugins/managedClusters/migration.go | 12 +
.../plugins/managedClusters/mock_dao.go | 2 +-
.../plugins/managedClusters/model.go | 1 +
.../plugins/managedClusters/plugin.go | 1 +
.../plugins/managedClusters/presenter.go | 7 +
.../registration_integration_test.go | 34 +-
.../plugins/managedClusters/service.go | 10 +-
.../rbac/cluster_binding_integration_test.go | 23 +-
.../roleBindings/grpc_integration_test.go | 16 +-
.../proto/hypershell/v1/gateways.proto | 15 +
.../test/authz/authz_integration_test.go | 13 +-
.../cmd/hypershell-controller/main.go | 2 +-
.../control-plane/internal/config/config.go | 14 +-
.../internal/config/config_test.go | 2 +
.../control-plane/internal/helm/values.go | 5 +
.../internal/registration/client.go | 14 +-
components/sdk-go/client/agent_runtime_api.go | 2 +-
components/sdk-go/client/client.go | 2 +-
components/sdk-go/client/gateway_api.go | 10 +-
.../sdk-go/client/inference_route_api.go | 2 +-
components/sdk-go/client/iterator.go | 2 +-
.../sdk-go/client/managed_cluster_api.go | 2 +-
.../open_shell_gateway_service_account_api.go | 12 +-
.../sdk-go/client/provider_binding_api.go | 2 +-
components/sdk-go/client/provider_spec_api.go | 2 +-
components/sdk-go/client/role_api.go | 2 +-
components/sdk-go/client/role_binding_api.go | 2 +-
.../sdk-go/client/sandbox_template_api.go | 2 +-
components/sdk-go/client/secret_source_api.go | 2 +-
components/sdk-go/client/user_api.go | 2 +-
components/sdk-go/types/agent_runtime.go | 2 +-
components/sdk-go/types/base.go | 2 +-
components/sdk-go/types/gateway.go | 48 +-
components/sdk-go/types/inference_route.go | 2 +-
components/sdk-go/types/list_options.go | 2 +-
components/sdk-go/types/managed_cluster.go | 3 +-
.../open_shell_gateway_service_account.go | 2 +-
components/sdk-go/types/provider_binding.go | 2 +-
components/sdk-go/types/provider_spec.go | 2 +-
components/sdk-go/types/role.go | 2 +-
components/sdk-go/types/role_binding.go | 2 +-
components/sdk-go/types/sandbox_template.go | 2 +-
components/sdk-go/types/secret_source.go | 2 +-
components/sdk-go/types/user.go | 2 +-
.../sdk-typescript/src/agent_runtime.ts | 2 +-
.../sdk-typescript/src/agent_runtime_api.ts | 2 +-
components/sdk-typescript/src/base.ts | 2 +-
components/sdk-typescript/src/client.ts | 2 +-
components/sdk-typescript/src/gateway.ts | 45 +-
components/sdk-typescript/src/gateway_api.ts | 8 +-
components/sdk-typescript/src/index.ts | 2 +-
.../sdk-typescript/src/inference_route.ts | 2 +-
.../sdk-typescript/src/inference_route_api.ts | 2 +-
.../sdk-typescript/src/managed_cluster.ts | 3 +-
.../sdk-typescript/src/managed_cluster_api.ts | 2 +-
.../src/open_shell_gateway_service_account.ts | 2 +-
.../open_shell_gateway_service_account_api.ts | 12 +-
.../sdk-typescript/src/provider_binding.ts | 2 +-
.../src/provider_binding_api.ts | 2 +-
.../sdk-typescript/src/provider_spec.ts | 2 +-
.../sdk-typescript/src/provider_spec_api.ts | 2 +-
components/sdk-typescript/src/role.ts | 2 +-
components/sdk-typescript/src/role_api.ts | 2 +-
components/sdk-typescript/src/role_binding.ts | 2 +-
.../sdk-typescript/src/role_binding_api.ts | 2 +-
.../sdk-typescript/src/sandbox_template.ts | 2 +-
.../src/sandbox_template_api.ts | 2 +-
.../sdk-typescript/src/secret_source.ts | 2 +-
.../sdk-typescript/src/secret_source_api.ts | 2 +-
components/sdk-typescript/src/user.ts | 2 +-
components/sdk-typescript/src/user_api.ts | 2 +-
.../adapters/api/gateway-operations.test.ts | 42 +-
.../app/adapters/api/gateway-operations.ts | 38 +-
.../gateways/gateway-create.stories.tsx | 79 +++
components/web-console/locales/en.json | 72 +++
deploy/base/applications/api-server.yaml | 2 +
deploy/base/control-plane/deployment.yaml | 4 +
.../base/platform-resources/controller.yaml | 4 +
deploy/gitops/platform/kustomization.yaml | 4 +
deploy/ibm/kustomization.yaml | 4 +
deploy/kind/kustomization.yaml | 29 +
deploy/openshift/kustomization.yaml | 4 +
.../src/application/gateway-operations.ts | 9 +
.../src/application/gateway-probes.ts | 1 +
.../src/application/gateway-types.ts | 29 +-
.../src/assets/aws-logo.svg | 38 ++
.../src/assets/ibm-cloud.svg | 1 +
.../src/gateways/gateway-create.module.css | 22 +
.../src/gateways/gateway-create.test.tsx | 545 ++++++------------
.../src/gateways/gateway-create.tsx | 484 ++++++++++++++--
.../src/gateways/gateway-placement-select.tsx | 392 -------------
packages/gateway-management-ui/src/index.ts | 1 +
.../gateway-management-ui/src/messages.ts | 101 ++++
scripts/sdk-generator/model.go | 1 +
scripts/sdk-generator/parser.go | 109 +++-
scripts/sdk-generator/parser_test.go | 90 ++-
.../sdk-generator/templates/go/client.go.tmpl | 10 +-
.../sdk-generator/templates/go/types.go.tmpl | 35 +-
.../sdk-generator/templates/ts/client.ts.tmpl | 8 +-
.../sdk-generator/templates/ts/types.ts.tmpl | 33 +-
skills/RECONCILE.md | 88 ++-
136 files changed, 4024 insertions(+), 1292 deletions(-)
create mode 100644 components/api-server/pkg/api/openapi/docs/GatewayPlacementAvailability.md
create mode 100644 components/api-server/pkg/api/openapi/docs/GatewayPlacementIntent.md
create mode 100644 components/api-server/pkg/api/openapi/model_gateway_placement_availability.go
create mode 100644 components/api-server/pkg/api/openapi/model_gateway_placement_intent.go
create mode 100644 components/api-server/plugins/gateways/availability_handler_test.go
create mode 100644 components/api-server/plugins/gateways/placement.go
create mode 100644 components/api-server/plugins/gateways/placement_test.go
create mode 100644 components/web-console/app/features/gateways/gateway-create.stories.tsx
create mode 100644 packages/gateway-management-ui/src/assets/aws-logo.svg
create mode 100644 packages/gateway-management-ui/src/assets/ibm-cloud.svg
create mode 100644 packages/gateway-management-ui/src/gateways/gateway-create.module.css
delete mode 100644 packages/gateway-management-ui/src/gateways/gateway-placement-select.tsx
diff --git a/components/api-server/go.mod b/components/api-server/go.mod
index 2d5ec7824..6f7c8b0c1 100644
--- a/components/api-server/go.mod
+++ b/components/api-server/go.mod
@@ -64,6 +64,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
@@ -112,6 +113,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
diff --git a/components/api-server/go.sum b/components/api-server/go.sum
index 89159e86d..c8c408066 100644
--- a/components/api-server/go.sum
+++ b/components/api-server/go.sum
@@ -88,8 +88,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@@ -302,8 +303,9 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -514,8 +516,8 @@ golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/components/api-server/openapi/openapi.gateways.yaml b/components/api-server/openapi/openapi.gateways.yaml
index 759aa7d25..9f559cf2a 100644
--- a/components/api-server/openapi/openapi.gateways.yaml
+++ b/components/api-server/openapi/openapi.gateways.yaml
@@ -59,9 +59,9 @@ paths:
$ref: '#/components/schemas/Gateway'
'400':
description: |
- Validation errors occurred, including a cluster_id that is empty or does not
- reference a ManagedCluster with a registered control plane (non-empty
- oidc_subject). On update, cluster_id is validated only when it changes.
+ Validation errors occurred, including an invalid or unavailable placement
+ intent. The backend resolves a valid intent to a registered ManagedCluster
+ before creating the gateway.
content:
application/json:
schema:
@@ -90,6 +90,31 @@ paths:
application/json:
schema:
$ref: 'openapi.yaml#/components/schemas/Error'
+ /api/hypershell/v1/gateways/placement-availability:
+ get:
+ operationId: getGatewayPlacementAvailability
+ summary: Returns current provider placement eligibility
+ security:
+ - Bearer: []
+ responses:
+ '200':
+ description: Current backend placement eligibility
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GatewayPlacementAvailability'
+ '401':
+ description: Auth token is invalid
+ content:
+ application/json:
+ schema:
+ $ref: 'openapi.yaml#/components/schemas/Error'
+ '500':
+ description: Unexpected error evaluating placement availability
+ content:
+ application/json:
+ schema:
+ $ref: 'openapi.yaml#/components/schemas/Error'
# NEW ENDPOINT START
/api/hypershell/v1/gateways/{id}:
# NEW ENDPOINT END
@@ -329,12 +354,12 @@ components:
type: object
required:
- name
- - cluster_id
+ - placement
properties:
name:
type: string
- cluster_id:
- type: string
+ placement:
+ $ref: '#/components/schemas/GatewayPlacementIntent'
external_dns:
type: string
tls_mode:
@@ -365,6 +390,51 @@ components:
credential_driver:
type: string
description: JSON-encoded credential storage driver configuration
+ GatewayPlacementIntent:
+ type: object
+ description: Exactly one of network/provider or mode must be supplied.
+ oneOf:
+ - title: Managed placement
+ required: [network, provider]
+ not:
+ required: [mode]
+ - title: Local Kind placement
+ required: [mode]
+ not:
+ anyOf:
+ - required: [network]
+ - required: [provider]
+ properties:
+ network:
+ type: string
+ enum: [public, vpn]
+ provider:
+ type: string
+ enum: [aws, ibm]
+ description: IBM Cloud supports public placement only; VPN requires AWS.
+ mode:
+ type: string
+ enum: [local-kind]
+ GatewayPlacementAvailability:
+ type: object
+ required: [aws_public, aws_vpn, ibm_public, ibm_vpn, local_kind]
+ properties:
+ aws_public:
+ type: boolean
+ aws_vpn:
+ type: boolean
+ ibm_public:
+ type: boolean
+ ibm_vpn:
+ type: boolean
+ local_kind:
+ type: boolean
+ aws_reason:
+ type: string
+ enum: [no-eligible-cluster]
+ ibm_reason:
+ type: string
+ enum: [no-eligible-cluster]
# NEW SCHEMA START
GatewayList:
# NEW SCHEMA END
diff --git a/components/api-server/openapi/openapi.managedClusters.yaml b/components/api-server/openapi/openapi.managedClusters.yaml
index fbcc84b91..6597dba4e 100644
--- a/components/api-server/openapi/openapi.managedClusters.yaml
+++ b/components/api-server/openapi/openapi.managedClusters.yaml
@@ -301,6 +301,12 @@ components:
type: string
provider:
type: string
+ description: Cloud provider capability used for gateway placement.
+ visibility:
+ type: string
+ enum: [public, vpn]
+ readOnly: true
+ description: Network visibility supported by this control plane for gateway placement.
region:
type: string
kubeconfig_secret:
@@ -322,6 +328,8 @@ components:
type: object
required:
- name
+ - provider
+ - visibility
properties:
name:
type: string
@@ -329,6 +337,14 @@ components:
description:
type: string
description: Optional description of the spoke.
+ provider:
+ type: string
+ enum: [aws, ibm, kind]
+ description: Cloud provider hosting this control plane.
+ visibility:
+ type: string
+ enum: [public, vpn]
+ description: Network visibility supported for gateways on this control plane.
ManagedClusterRegistrationResponse:
type: object
required:
diff --git a/components/api-server/openapi/openapi.yaml b/components/api-server/openapi/openapi.yaml
index e37155f45..4d99919ad 100644
--- a/components/api-server/openapi/openapi.yaml
+++ b/components/api-server/openapi/openapi.yaml
@@ -34,6 +34,8 @@ paths:
$ref: 'openapi.managedClusters.yaml#/paths/~1api~1hypershell~1v1~1managed_clusters~1{id}'
/api/hypershell/v1/gateways:
$ref: 'openapi.gateways.yaml#/paths/~1api~1hypershell~1v1~1gateways'
+ /api/hypershell/v1/gateways/placement-availability:
+ $ref: 'openapi.gateways.yaml#/paths/~1api~1hypershell~1v1~1gateways~1placement-availability'
/api/hypershell/v1/gateways/{id}:
$ref: 'openapi.gateways.yaml#/paths/~1api~1hypershell~1v1~1gateways~1{id}'
/api/hypershell/v1/gateways/{gateway_id}/service_accounts:
@@ -136,6 +138,10 @@ components:
$ref: 'openapi.gateways.yaml#/components/schemas/GatewayList'
GatewayPatchRequest:
$ref: 'openapi.gateways.yaml#/components/schemas/GatewayPatchRequest'
+ GatewayPlacementIntent:
+ $ref: 'openapi.gateways.yaml#/components/schemas/GatewayPlacementIntent'
+ GatewayPlacementAvailability:
+ $ref: 'openapi.gateways.yaml#/components/schemas/GatewayPlacementAvailability'
OpenShellGatewayServiceAccountRole:
$ref: 'openapi.serviceAccounts.yaml#/components/schemas/OpenShellGatewayServiceAccountRole'
OpenShellGatewayServiceAccountStatus:
diff --git a/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go b/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go
index b5ed58aea..e09b35045 100644
--- a/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go
+++ b/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go
@@ -136,6 +136,176 @@ func (x *ProvisioningCondition) GetMessage() string {
return ""
}
+type GatewayPlacementIntent struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Types that are valid to be assigned to Placement:
+ //
+ // *GatewayPlacementIntent_NetworkProvider
+ // *GatewayPlacementIntent_LocalKind
+ Placement isGatewayPlacementIntent_Placement `protobuf_oneof:"placement"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GatewayPlacementIntent) Reset() {
+ *x = GatewayPlacementIntent{}
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GatewayPlacementIntent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GatewayPlacementIntent) ProtoMessage() {}
+
+func (x *GatewayPlacementIntent) ProtoReflect() protoreflect.Message {
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GatewayPlacementIntent.ProtoReflect.Descriptor instead.
+func (*GatewayPlacementIntent) Descriptor() ([]byte, []int) {
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *GatewayPlacementIntent) GetPlacement() isGatewayPlacementIntent_Placement {
+ if x != nil {
+ return x.Placement
+ }
+ return nil
+}
+
+func (x *GatewayPlacementIntent) GetNetworkProvider() *NetworkProviderPlacement {
+ if x != nil {
+ if x, ok := x.Placement.(*GatewayPlacementIntent_NetworkProvider); ok {
+ return x.NetworkProvider
+ }
+ }
+ return nil
+}
+
+func (x *GatewayPlacementIntent) GetLocalKind() *LocalKindPlacement {
+ if x != nil {
+ if x, ok := x.Placement.(*GatewayPlacementIntent_LocalKind); ok {
+ return x.LocalKind
+ }
+ }
+ return nil
+}
+
+type isGatewayPlacementIntent_Placement interface {
+ isGatewayPlacementIntent_Placement()
+}
+
+type GatewayPlacementIntent_NetworkProvider struct {
+ NetworkProvider *NetworkProviderPlacement `protobuf:"bytes,1,opt,name=network_provider,json=networkProvider,proto3,oneof"`
+}
+
+type GatewayPlacementIntent_LocalKind struct {
+ LocalKind *LocalKindPlacement `protobuf:"bytes,2,opt,name=local_kind,json=localKind,proto3,oneof"`
+}
+
+func (*GatewayPlacementIntent_NetworkProvider) isGatewayPlacementIntent_Placement() {}
+
+func (*GatewayPlacementIntent_LocalKind) isGatewayPlacementIntent_Placement() {}
+
+type NetworkProviderPlacement struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"`
+ Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *NetworkProviderPlacement) Reset() {
+ *x = NetworkProviderPlacement{}
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *NetworkProviderPlacement) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NetworkProviderPlacement) ProtoMessage() {}
+
+func (x *NetworkProviderPlacement) ProtoReflect() protoreflect.Message {
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use NetworkProviderPlacement.ProtoReflect.Descriptor instead.
+func (*NetworkProviderPlacement) Descriptor() ([]byte, []int) {
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *NetworkProviderPlacement) GetNetwork() string {
+ if x != nil {
+ return x.Network
+ }
+ return ""
+}
+
+func (x *NetworkProviderPlacement) GetProvider() string {
+ if x != nil {
+ return x.Provider
+ }
+ return ""
+}
+
+type LocalKindPlacement struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *LocalKindPlacement) Reset() {
+ *x = LocalKindPlacement{}
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *LocalKindPlacement) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LocalKindPlacement) ProtoMessage() {}
+
+func (x *LocalKindPlacement) ProtoReflect() protoreflect.Message {
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LocalKindPlacement.ProtoReflect.Descriptor instead.
+func (*LocalKindPlacement) Descriptor() ([]byte, []int) {
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{3}
+}
+
type Gateway struct {
state protoimpl.MessageState `protogen:"open.v1"`
Metadata *ObjectReference `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
@@ -172,7 +342,7 @@ type Gateway struct {
func (x *Gateway) Reset() {
*x = Gateway{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[1]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -184,7 +354,7 @@ func (x *Gateway) String() string {
func (*Gateway) ProtoMessage() {}
func (x *Gateway) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[1]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -197,7 +367,7 @@ func (x *Gateway) ProtoReflect() protoreflect.Message {
// Deprecated: Use Gateway.ProtoReflect.Descriptor instead.
func (*Gateway) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{1}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{4}
}
func (x *Gateway) GetMetadata() *ObjectReference {
@@ -369,28 +539,29 @@ func (x *Gateway) GetObservedGeneration() int64 {
}
type CreateGatewayRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
- ClusterId string `protobuf:"bytes,3,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"`
- ReleaseId string `protobuf:"bytes,4,opt,name=release_id,json=releaseId,proto3" json:"release_id,omitempty"`
- ExternalDns *string `protobuf:"bytes,6,opt,name=external_dns,json=externalDns,proto3,oneof" json:"external_dns,omitempty"`
- TlsMode *string `protobuf:"bytes,7,opt,name=tls_mode,json=tlsMode,proto3,oneof" json:"tls_mode,omitempty"`
- ServiceType *string `protobuf:"bytes,8,opt,name=service_type,json=serviceType,proto3,oneof" json:"service_type,omitempty"`
- Status *string `protobuf:"bytes,9,opt,name=status,proto3,oneof" json:"status,omitempty"`
- Phase *string `protobuf:"bytes,10,opt,name=phase,proto3,oneof" json:"phase,omitempty"`
- Image *string `protobuf:"bytes,11,opt,name=image,proto3,oneof" json:"image,omitempty"`
- SupervisorImage *string `protobuf:"bytes,12,opt,name=supervisor_image,json=supervisorImage,proto3,oneof" json:"supervisor_image,omitempty"`
- ServerDnsNames []string `protobuf:"bytes,13,rep,name=server_dns_names,json=serverDnsNames,proto3" json:"server_dns_names,omitempty"`
- Oidc *string `protobuf:"bytes,14,opt,name=oidc,proto3,oneof" json:"oidc,omitempty"`
- Route *string `protobuf:"bytes,15,opt,name=route,proto3,oneof" json:"route,omitempty"`
- CredentialDriver *string `protobuf:"bytes,17,opt,name=credential_driver,json=credentialDriver,proto3,oneof" json:"credential_driver,omitempty"`
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ ClusterId string `protobuf:"bytes,3,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"`
+ ReleaseId string `protobuf:"bytes,4,opt,name=release_id,json=releaseId,proto3" json:"release_id,omitempty"`
+ ExternalDns *string `protobuf:"bytes,6,opt,name=external_dns,json=externalDns,proto3,oneof" json:"external_dns,omitempty"`
+ TlsMode *string `protobuf:"bytes,7,opt,name=tls_mode,json=tlsMode,proto3,oneof" json:"tls_mode,omitempty"`
+ ServiceType *string `protobuf:"bytes,8,opt,name=service_type,json=serviceType,proto3,oneof" json:"service_type,omitempty"`
+ Status *string `protobuf:"bytes,9,opt,name=status,proto3,oneof" json:"status,omitempty"`
+ Phase *string `protobuf:"bytes,10,opt,name=phase,proto3,oneof" json:"phase,omitempty"`
+ Image *string `protobuf:"bytes,11,opt,name=image,proto3,oneof" json:"image,omitempty"`
+ SupervisorImage *string `protobuf:"bytes,12,opt,name=supervisor_image,json=supervisorImage,proto3,oneof" json:"supervisor_image,omitempty"`
+ ServerDnsNames []string `protobuf:"bytes,13,rep,name=server_dns_names,json=serverDnsNames,proto3" json:"server_dns_names,omitempty"`
+ Oidc *string `protobuf:"bytes,14,opt,name=oidc,proto3,oneof" json:"oidc,omitempty"`
+ Route *string `protobuf:"bytes,15,opt,name=route,proto3,oneof" json:"route,omitempty"`
+ CredentialDriver *string `protobuf:"bytes,17,opt,name=credential_driver,json=credentialDriver,proto3,oneof" json:"credential_driver,omitempty"`
+ Placement *GatewayPlacementIntent `protobuf:"bytes,18,opt,name=placement,proto3" json:"placement,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateGatewayRequest) Reset() {
*x = CreateGatewayRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[2]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -402,7 +573,7 @@ func (x *CreateGatewayRequest) String() string {
func (*CreateGatewayRequest) ProtoMessage() {}
func (x *CreateGatewayRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[2]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -415,7 +586,7 @@ func (x *CreateGatewayRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateGatewayRequest.ProtoReflect.Descriptor instead.
func (*CreateGatewayRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{2}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{5}
}
func (x *CreateGatewayRequest) GetName() string {
@@ -516,6 +687,13 @@ func (x *CreateGatewayRequest) GetCredentialDriver() string {
return ""
}
+func (x *CreateGatewayRequest) GetPlacement() *GatewayPlacementIntent {
+ if x != nil {
+ return x.Placement
+ }
+ return nil
+}
+
type CreateGatewayResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Gateway *Gateway `protobuf:"bytes,1,opt,name=gateway,proto3" json:"gateway,omitempty"`
@@ -525,7 +703,7 @@ type CreateGatewayResponse struct {
func (x *CreateGatewayResponse) Reset() {
*x = CreateGatewayResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[3]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -537,7 +715,7 @@ func (x *CreateGatewayResponse) String() string {
func (*CreateGatewayResponse) ProtoMessage() {}
func (x *CreateGatewayResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[3]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -550,7 +728,7 @@ func (x *CreateGatewayResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateGatewayResponse.ProtoReflect.Descriptor instead.
func (*CreateGatewayResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{3}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{6}
}
func (x *CreateGatewayResponse) GetGateway() *Gateway {
@@ -569,7 +747,7 @@ type GetGatewayRequest struct {
func (x *GetGatewayRequest) Reset() {
*x = GetGatewayRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[4]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -581,7 +759,7 @@ func (x *GetGatewayRequest) String() string {
func (*GetGatewayRequest) ProtoMessage() {}
func (x *GetGatewayRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[4]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -594,7 +772,7 @@ func (x *GetGatewayRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetGatewayRequest.ProtoReflect.Descriptor instead.
func (*GetGatewayRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{4}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{7}
}
func (x *GetGatewayRequest) GetId() string {
@@ -613,7 +791,7 @@ type GetGatewayResponse struct {
func (x *GetGatewayResponse) Reset() {
*x = GetGatewayResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[5]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -625,7 +803,7 @@ func (x *GetGatewayResponse) String() string {
func (*GetGatewayResponse) ProtoMessage() {}
func (x *GetGatewayResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[5]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -638,7 +816,7 @@ func (x *GetGatewayResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetGatewayResponse.ProtoReflect.Descriptor instead.
func (*GetGatewayResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{5}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{8}
}
func (x *GetGatewayResponse) GetGateway() *Gateway {
@@ -684,7 +862,7 @@ type UpdateGatewayRequest struct {
func (x *UpdateGatewayRequest) Reset() {
*x = UpdateGatewayRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[6]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -696,7 +874,7 @@ func (x *UpdateGatewayRequest) String() string {
func (*UpdateGatewayRequest) ProtoMessage() {}
func (x *UpdateGatewayRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[6]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -709,7 +887,7 @@ func (x *UpdateGatewayRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateGatewayRequest.ProtoReflect.Descriptor instead.
func (*UpdateGatewayRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{6}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{9}
}
func (x *UpdateGatewayRequest) GetId() string {
@@ -861,7 +1039,7 @@ type UpdateGatewayResponse struct {
func (x *UpdateGatewayResponse) Reset() {
*x = UpdateGatewayResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[7]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -873,7 +1051,7 @@ func (x *UpdateGatewayResponse) String() string {
func (*UpdateGatewayResponse) ProtoMessage() {}
func (x *UpdateGatewayResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[7]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -886,7 +1064,7 @@ func (x *UpdateGatewayResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateGatewayResponse.ProtoReflect.Descriptor instead.
func (*UpdateGatewayResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{7}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{10}
}
func (x *UpdateGatewayResponse) GetGateway() *Gateway {
@@ -909,7 +1087,7 @@ type AdjustActiveSandboxCountRequest struct {
func (x *AdjustActiveSandboxCountRequest) Reset() {
*x = AdjustActiveSandboxCountRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[8]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -921,7 +1099,7 @@ func (x *AdjustActiveSandboxCountRequest) String() string {
func (*AdjustActiveSandboxCountRequest) ProtoMessage() {}
func (x *AdjustActiveSandboxCountRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[8]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -934,7 +1112,7 @@ func (x *AdjustActiveSandboxCountRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdjustActiveSandboxCountRequest.ProtoReflect.Descriptor instead.
func (*AdjustActiveSandboxCountRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{8}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{11}
}
func (x *AdjustActiveSandboxCountRequest) GetNamespace() string {
@@ -962,7 +1140,7 @@ type AdjustActiveSandboxCountResponse struct {
func (x *AdjustActiveSandboxCountResponse) Reset() {
*x = AdjustActiveSandboxCountResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[9]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -974,7 +1152,7 @@ func (x *AdjustActiveSandboxCountResponse) String() string {
func (*AdjustActiveSandboxCountResponse) ProtoMessage() {}
func (x *AdjustActiveSandboxCountResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[9]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -987,7 +1165,7 @@ func (x *AdjustActiveSandboxCountResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdjustActiveSandboxCountResponse.ProtoReflect.Descriptor instead.
func (*AdjustActiveSandboxCountResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{9}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{12}
}
func (x *AdjustActiveSandboxCountResponse) GetActiveSandboxCount() int32 {
@@ -1010,7 +1188,7 @@ type SetActiveSandboxCountRequest struct {
func (x *SetActiveSandboxCountRequest) Reset() {
*x = SetActiveSandboxCountRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[10]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1022,7 +1200,7 @@ func (x *SetActiveSandboxCountRequest) String() string {
func (*SetActiveSandboxCountRequest) ProtoMessage() {}
func (x *SetActiveSandboxCountRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[10]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1035,7 +1213,7 @@ func (x *SetActiveSandboxCountRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetActiveSandboxCountRequest.ProtoReflect.Descriptor instead.
func (*SetActiveSandboxCountRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{10}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{13}
}
func (x *SetActiveSandboxCountRequest) GetNamespace() string {
@@ -1061,7 +1239,7 @@ type SetActiveSandboxCountResponse struct {
func (x *SetActiveSandboxCountResponse) Reset() {
*x = SetActiveSandboxCountResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[11]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1073,7 +1251,7 @@ func (x *SetActiveSandboxCountResponse) String() string {
func (*SetActiveSandboxCountResponse) ProtoMessage() {}
func (x *SetActiveSandboxCountResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[11]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1086,7 +1264,7 @@ func (x *SetActiveSandboxCountResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetActiveSandboxCountResponse.ProtoReflect.Descriptor instead.
func (*SetActiveSandboxCountResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{11}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{14}
}
func (x *SetActiveSandboxCountResponse) GetActiveSandboxCount() int32 {
@@ -1108,7 +1286,7 @@ type SetGatewayVersionRequest struct {
func (x *SetGatewayVersionRequest) Reset() {
*x = SetGatewayVersionRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[12]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1120,7 +1298,7 @@ func (x *SetGatewayVersionRequest) String() string {
func (*SetGatewayVersionRequest) ProtoMessage() {}
func (x *SetGatewayVersionRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[12]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1133,7 +1311,7 @@ func (x *SetGatewayVersionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetGatewayVersionRequest.ProtoReflect.Descriptor instead.
func (*SetGatewayVersionRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{12}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{15}
}
func (x *SetGatewayVersionRequest) GetId() string {
@@ -1159,7 +1337,7 @@ type SetGatewayVersionResponse struct {
func (x *SetGatewayVersionResponse) Reset() {
*x = SetGatewayVersionResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[13]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1171,7 +1349,7 @@ func (x *SetGatewayVersionResponse) String() string {
func (*SetGatewayVersionResponse) ProtoMessage() {}
func (x *SetGatewayVersionResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[13]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1184,7 +1362,7 @@ func (x *SetGatewayVersionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetGatewayVersionResponse.ProtoReflect.Descriptor instead.
func (*SetGatewayVersionResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{13}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{16}
}
func (x *SetGatewayVersionResponse) GetGatewayVersion() string {
@@ -1203,7 +1381,7 @@ type DeleteGatewayRequest struct {
func (x *DeleteGatewayRequest) Reset() {
*x = DeleteGatewayRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[14]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1215,7 +1393,7 @@ func (x *DeleteGatewayRequest) String() string {
func (*DeleteGatewayRequest) ProtoMessage() {}
func (x *DeleteGatewayRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[14]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1228,7 +1406,7 @@ func (x *DeleteGatewayRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteGatewayRequest.ProtoReflect.Descriptor instead.
func (*DeleteGatewayRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{14}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{17}
}
func (x *DeleteGatewayRequest) GetId() string {
@@ -1253,7 +1431,7 @@ type ListGatewaysRequest struct {
func (x *ListGatewaysRequest) Reset() {
*x = ListGatewaysRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[15]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1265,7 +1443,7 @@ func (x *ListGatewaysRequest) String() string {
func (*ListGatewaysRequest) ProtoMessage() {}
func (x *ListGatewaysRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[15]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1278,7 +1456,7 @@ func (x *ListGatewaysRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListGatewaysRequest.ProtoReflect.Descriptor instead.
func (*ListGatewaysRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{15}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{18}
}
func (x *ListGatewaysRequest) GetPage() int32 {
@@ -1312,7 +1490,7 @@ type ListGatewaysResponse struct {
func (x *ListGatewaysResponse) Reset() {
*x = ListGatewaysResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[16]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1324,7 +1502,7 @@ func (x *ListGatewaysResponse) String() string {
func (*ListGatewaysResponse) ProtoMessage() {}
func (x *ListGatewaysResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[16]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1337,7 +1515,7 @@ func (x *ListGatewaysResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListGatewaysResponse.ProtoReflect.Descriptor instead.
func (*ListGatewaysResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{16}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{19}
}
func (x *ListGatewaysResponse) GetItems() []*Gateway {
@@ -1362,7 +1540,7 @@ type DeleteGatewayResponse struct {
func (x *DeleteGatewayResponse) Reset() {
*x = DeleteGatewayResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[17]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1374,7 +1552,7 @@ func (x *DeleteGatewayResponse) String() string {
func (*DeleteGatewayResponse) ProtoMessage() {}
func (x *DeleteGatewayResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[17]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1387,7 +1565,7 @@ func (x *DeleteGatewayResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteGatewayResponse.ProtoReflect.Descriptor instead.
func (*DeleteGatewayResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{17}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{20}
}
type WatchGatewaysRequest struct {
@@ -1403,7 +1581,7 @@ type WatchGatewaysRequest struct {
func (x *WatchGatewaysRequest) Reset() {
*x = WatchGatewaysRequest{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[18]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1415,7 +1593,7 @@ func (x *WatchGatewaysRequest) String() string {
func (*WatchGatewaysRequest) ProtoMessage() {}
func (x *WatchGatewaysRequest) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[18]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1428,7 +1606,7 @@ func (x *WatchGatewaysRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use WatchGatewaysRequest.ProtoReflect.Descriptor instead.
func (*WatchGatewaysRequest) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{18}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{21}
}
func (x *WatchGatewaysRequest) GetClusterId() string {
@@ -1449,7 +1627,7 @@ type WatchGatewaysResponse struct {
func (x *WatchGatewaysResponse) Reset() {
*x = WatchGatewaysResponse{}
- mi := &file_hypershell_v1_gateways_proto_msgTypes[19]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1461,7 +1639,7 @@ func (x *WatchGatewaysResponse) String() string {
func (*WatchGatewaysResponse) ProtoMessage() {}
func (x *WatchGatewaysResponse) ProtoReflect() protoreflect.Message {
- mi := &file_hypershell_v1_gateways_proto_msgTypes[19]
+ mi := &file_hypershell_v1_gateways_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1474,7 +1652,7 @@ func (x *WatchGatewaysResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use WatchGatewaysResponse.ProtoReflect.Descriptor instead.
func (*WatchGatewaysResponse) Descriptor() ([]byte, []int) {
- return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{19}
+ return file_hypershell_v1_gateways_proto_rawDescGZIP(), []int{22}
}
func (x *WatchGatewaysResponse) GetType() EventType {
@@ -1506,7 +1684,16 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" +
"\x15ProvisioningCondition\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12U\n" +
"\x10condition_status\x18\x02 \x01(\x0e2*.hypershell.v1.ProvisioningConditionStatusR\x0fconditionStatus\x12\x18\n" +
- "\amessage\x18\x03 \x01(\tR\amessage\"\x91\n" +
+ "\amessage\x18\x03 \x01(\tR\amessage\"\xbf\x01\n" +
+ "\x16GatewayPlacementIntent\x12T\n" +
+ "\x10network_provider\x18\x01 \x01(\v2'.hypershell.v1.NetworkProviderPlacementH\x00R\x0fnetworkProvider\x12B\n" +
+ "\n" +
+ "local_kind\x18\x02 \x01(\v2!.hypershell.v1.LocalKindPlacementH\x00R\tlocalKindB\v\n" +
+ "\tplacement\"P\n" +
+ "\x18NetworkProviderPlacement\x12\x18\n" +
+ "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x1a\n" +
+ "\bprovider\x18\x02 \x01(\tR\bprovider\"\x14\n" +
+ "\x12LocalKindPlacement\"\x91\n" +
"\n" +
"\aGateway\x12:\n" +
"\bmetadata\x18\x01 \x01(\v2\x1e.hypershell.v1.ObjectReferenceR\bmetadata\x12\x12\n" +
@@ -1554,7 +1741,7 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" +
"\x10_console_addressB\x12\n" +
"\x10_gateway_versionB\x16\n" +
"\x14_observed_release_idB\x16\n" +
- "\x14_observed_generationJ\x04\b\x03\x10\x04J\x04\b\x06\x10\aR\bfleet_idR\vdatabase_id\"\x9a\x05\n" +
+ "\x14_observed_generationJ\x04\b\x03\x10\x04J\x04\b\x06\x10\aR\bfleet_idR\vdatabase_id\"\xdf\x05\n" +
"\x14CreateGatewayRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
@@ -1572,7 +1759,8 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" +
"\x10server_dns_names\x18\r \x03(\tR\x0eserverDnsNames\x12\x17\n" +
"\x04oidc\x18\x0e \x01(\tH\aR\x04oidc\x88\x01\x01\x12\x19\n" +
"\x05route\x18\x0f \x01(\tH\bR\x05route\x88\x01\x01\x120\n" +
- "\x11credential_driver\x18\x11 \x01(\tH\tR\x10credentialDriver\x88\x01\x01B\x0f\n" +
+ "\x11credential_driver\x18\x11 \x01(\tH\tR\x10credentialDriver\x88\x01\x01\x12C\n" +
+ "\tplacement\x18\x12 \x01(\v2%.hypershell.v1.GatewayPlacementIntentR\tplacementB\x0f\n" +
"\r_external_dnsB\v\n" +
"\t_tls_modeB\x0f\n" +
"\r_service_typeB\t\n" +
@@ -1700,68 +1888,74 @@ func file_hypershell_v1_gateways_proto_rawDescGZIP() []byte {
}
var file_hypershell_v1_gateways_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_hypershell_v1_gateways_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
+var file_hypershell_v1_gateways_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_hypershell_v1_gateways_proto_goTypes = []any{
(ProvisioningConditionStatus)(0), // 0: hypershell.v1.ProvisioningConditionStatus
(*ProvisioningCondition)(nil), // 1: hypershell.v1.ProvisioningCondition
- (*Gateway)(nil), // 2: hypershell.v1.Gateway
- (*CreateGatewayRequest)(nil), // 3: hypershell.v1.CreateGatewayRequest
- (*CreateGatewayResponse)(nil), // 4: hypershell.v1.CreateGatewayResponse
- (*GetGatewayRequest)(nil), // 5: hypershell.v1.GetGatewayRequest
- (*GetGatewayResponse)(nil), // 6: hypershell.v1.GetGatewayResponse
- (*UpdateGatewayRequest)(nil), // 7: hypershell.v1.UpdateGatewayRequest
- (*UpdateGatewayResponse)(nil), // 8: hypershell.v1.UpdateGatewayResponse
- (*AdjustActiveSandboxCountRequest)(nil), // 9: hypershell.v1.AdjustActiveSandboxCountRequest
- (*AdjustActiveSandboxCountResponse)(nil), // 10: hypershell.v1.AdjustActiveSandboxCountResponse
- (*SetActiveSandboxCountRequest)(nil), // 11: hypershell.v1.SetActiveSandboxCountRequest
- (*SetActiveSandboxCountResponse)(nil), // 12: hypershell.v1.SetActiveSandboxCountResponse
- (*SetGatewayVersionRequest)(nil), // 13: hypershell.v1.SetGatewayVersionRequest
- (*SetGatewayVersionResponse)(nil), // 14: hypershell.v1.SetGatewayVersionResponse
- (*DeleteGatewayRequest)(nil), // 15: hypershell.v1.DeleteGatewayRequest
- (*ListGatewaysRequest)(nil), // 16: hypershell.v1.ListGatewaysRequest
- (*ListGatewaysResponse)(nil), // 17: hypershell.v1.ListGatewaysResponse
- (*DeleteGatewayResponse)(nil), // 18: hypershell.v1.DeleteGatewayResponse
- (*WatchGatewaysRequest)(nil), // 19: hypershell.v1.WatchGatewaysRequest
- (*WatchGatewaysResponse)(nil), // 20: hypershell.v1.WatchGatewaysResponse
- (*ObjectReference)(nil), // 21: hypershell.v1.ObjectReference
- (*ListMeta)(nil), // 22: hypershell.v1.ListMeta
- (EventType)(0), // 23: hypershell.v1.EventType
+ (*GatewayPlacementIntent)(nil), // 2: hypershell.v1.GatewayPlacementIntent
+ (*NetworkProviderPlacement)(nil), // 3: hypershell.v1.NetworkProviderPlacement
+ (*LocalKindPlacement)(nil), // 4: hypershell.v1.LocalKindPlacement
+ (*Gateway)(nil), // 5: hypershell.v1.Gateway
+ (*CreateGatewayRequest)(nil), // 6: hypershell.v1.CreateGatewayRequest
+ (*CreateGatewayResponse)(nil), // 7: hypershell.v1.CreateGatewayResponse
+ (*GetGatewayRequest)(nil), // 8: hypershell.v1.GetGatewayRequest
+ (*GetGatewayResponse)(nil), // 9: hypershell.v1.GetGatewayResponse
+ (*UpdateGatewayRequest)(nil), // 10: hypershell.v1.UpdateGatewayRequest
+ (*UpdateGatewayResponse)(nil), // 11: hypershell.v1.UpdateGatewayResponse
+ (*AdjustActiveSandboxCountRequest)(nil), // 12: hypershell.v1.AdjustActiveSandboxCountRequest
+ (*AdjustActiveSandboxCountResponse)(nil), // 13: hypershell.v1.AdjustActiveSandboxCountResponse
+ (*SetActiveSandboxCountRequest)(nil), // 14: hypershell.v1.SetActiveSandboxCountRequest
+ (*SetActiveSandboxCountResponse)(nil), // 15: hypershell.v1.SetActiveSandboxCountResponse
+ (*SetGatewayVersionRequest)(nil), // 16: hypershell.v1.SetGatewayVersionRequest
+ (*SetGatewayVersionResponse)(nil), // 17: hypershell.v1.SetGatewayVersionResponse
+ (*DeleteGatewayRequest)(nil), // 18: hypershell.v1.DeleteGatewayRequest
+ (*ListGatewaysRequest)(nil), // 19: hypershell.v1.ListGatewaysRequest
+ (*ListGatewaysResponse)(nil), // 20: hypershell.v1.ListGatewaysResponse
+ (*DeleteGatewayResponse)(nil), // 21: hypershell.v1.DeleteGatewayResponse
+ (*WatchGatewaysRequest)(nil), // 22: hypershell.v1.WatchGatewaysRequest
+ (*WatchGatewaysResponse)(nil), // 23: hypershell.v1.WatchGatewaysResponse
+ (*ObjectReference)(nil), // 24: hypershell.v1.ObjectReference
+ (*ListMeta)(nil), // 25: hypershell.v1.ListMeta
+ (EventType)(0), // 26: hypershell.v1.EventType
}
var file_hypershell_v1_gateways_proto_depIdxs = []int32{
0, // 0: hypershell.v1.ProvisioningCondition.condition_status:type_name -> hypershell.v1.ProvisioningConditionStatus
- 21, // 1: hypershell.v1.Gateway.metadata:type_name -> hypershell.v1.ObjectReference
- 1, // 2: hypershell.v1.Gateway.provisioning_conditions:type_name -> hypershell.v1.ProvisioningCondition
- 2, // 3: hypershell.v1.CreateGatewayResponse.gateway:type_name -> hypershell.v1.Gateway
- 2, // 4: hypershell.v1.GetGatewayResponse.gateway:type_name -> hypershell.v1.Gateway
- 1, // 5: hypershell.v1.UpdateGatewayRequest.provisioning_conditions:type_name -> hypershell.v1.ProvisioningCondition
- 2, // 6: hypershell.v1.UpdateGatewayResponse.gateway:type_name -> hypershell.v1.Gateway
- 2, // 7: hypershell.v1.ListGatewaysResponse.items:type_name -> hypershell.v1.Gateway
- 22, // 8: hypershell.v1.ListGatewaysResponse.metadata:type_name -> hypershell.v1.ListMeta
- 23, // 9: hypershell.v1.WatchGatewaysResponse.type:type_name -> hypershell.v1.EventType
- 2, // 10: hypershell.v1.WatchGatewaysResponse.gateway:type_name -> hypershell.v1.Gateway
- 5, // 11: hypershell.v1.GatewayService.GetGateway:input_type -> hypershell.v1.GetGatewayRequest
- 3, // 12: hypershell.v1.GatewayService.CreateGateway:input_type -> hypershell.v1.CreateGatewayRequest
- 7, // 13: hypershell.v1.GatewayService.UpdateGateway:input_type -> hypershell.v1.UpdateGatewayRequest
- 15, // 14: hypershell.v1.GatewayService.DeleteGateway:input_type -> hypershell.v1.DeleteGatewayRequest
- 16, // 15: hypershell.v1.GatewayService.ListGateways:input_type -> hypershell.v1.ListGatewaysRequest
- 19, // 16: hypershell.v1.GatewayService.WatchGateways:input_type -> hypershell.v1.WatchGatewaysRequest
- 9, // 17: hypershell.v1.GatewayService.AdjustActiveSandboxCount:input_type -> hypershell.v1.AdjustActiveSandboxCountRequest
- 11, // 18: hypershell.v1.GatewayService.SetActiveSandboxCount:input_type -> hypershell.v1.SetActiveSandboxCountRequest
- 13, // 19: hypershell.v1.GatewayService.SetGatewayVersion:input_type -> hypershell.v1.SetGatewayVersionRequest
- 6, // 20: hypershell.v1.GatewayService.GetGateway:output_type -> hypershell.v1.GetGatewayResponse
- 4, // 21: hypershell.v1.GatewayService.CreateGateway:output_type -> hypershell.v1.CreateGatewayResponse
- 8, // 22: hypershell.v1.GatewayService.UpdateGateway:output_type -> hypershell.v1.UpdateGatewayResponse
- 18, // 23: hypershell.v1.GatewayService.DeleteGateway:output_type -> hypershell.v1.DeleteGatewayResponse
- 17, // 24: hypershell.v1.GatewayService.ListGateways:output_type -> hypershell.v1.ListGatewaysResponse
- 20, // 25: hypershell.v1.GatewayService.WatchGateways:output_type -> hypershell.v1.WatchGatewaysResponse
- 10, // 26: hypershell.v1.GatewayService.AdjustActiveSandboxCount:output_type -> hypershell.v1.AdjustActiveSandboxCountResponse
- 12, // 27: hypershell.v1.GatewayService.SetActiveSandboxCount:output_type -> hypershell.v1.SetActiveSandboxCountResponse
- 14, // 28: hypershell.v1.GatewayService.SetGatewayVersion:output_type -> hypershell.v1.SetGatewayVersionResponse
- 20, // [20:29] is the sub-list for method output_type
- 11, // [11:20] is the sub-list for method input_type
- 11, // [11:11] is the sub-list for extension type_name
- 11, // [11:11] is the sub-list for extension extendee
- 0, // [0:11] is the sub-list for field type_name
+ 3, // 1: hypershell.v1.GatewayPlacementIntent.network_provider:type_name -> hypershell.v1.NetworkProviderPlacement
+ 4, // 2: hypershell.v1.GatewayPlacementIntent.local_kind:type_name -> hypershell.v1.LocalKindPlacement
+ 24, // 3: hypershell.v1.Gateway.metadata:type_name -> hypershell.v1.ObjectReference
+ 1, // 4: hypershell.v1.Gateway.provisioning_conditions:type_name -> hypershell.v1.ProvisioningCondition
+ 2, // 5: hypershell.v1.CreateGatewayRequest.placement:type_name -> hypershell.v1.GatewayPlacementIntent
+ 5, // 6: hypershell.v1.CreateGatewayResponse.gateway:type_name -> hypershell.v1.Gateway
+ 5, // 7: hypershell.v1.GetGatewayResponse.gateway:type_name -> hypershell.v1.Gateway
+ 1, // 8: hypershell.v1.UpdateGatewayRequest.provisioning_conditions:type_name -> hypershell.v1.ProvisioningCondition
+ 5, // 9: hypershell.v1.UpdateGatewayResponse.gateway:type_name -> hypershell.v1.Gateway
+ 5, // 10: hypershell.v1.ListGatewaysResponse.items:type_name -> hypershell.v1.Gateway
+ 25, // 11: hypershell.v1.ListGatewaysResponse.metadata:type_name -> hypershell.v1.ListMeta
+ 26, // 12: hypershell.v1.WatchGatewaysResponse.type:type_name -> hypershell.v1.EventType
+ 5, // 13: hypershell.v1.WatchGatewaysResponse.gateway:type_name -> hypershell.v1.Gateway
+ 8, // 14: hypershell.v1.GatewayService.GetGateway:input_type -> hypershell.v1.GetGatewayRequest
+ 6, // 15: hypershell.v1.GatewayService.CreateGateway:input_type -> hypershell.v1.CreateGatewayRequest
+ 10, // 16: hypershell.v1.GatewayService.UpdateGateway:input_type -> hypershell.v1.UpdateGatewayRequest
+ 18, // 17: hypershell.v1.GatewayService.DeleteGateway:input_type -> hypershell.v1.DeleteGatewayRequest
+ 19, // 18: hypershell.v1.GatewayService.ListGateways:input_type -> hypershell.v1.ListGatewaysRequest
+ 22, // 19: hypershell.v1.GatewayService.WatchGateways:input_type -> hypershell.v1.WatchGatewaysRequest
+ 12, // 20: hypershell.v1.GatewayService.AdjustActiveSandboxCount:input_type -> hypershell.v1.AdjustActiveSandboxCountRequest
+ 14, // 21: hypershell.v1.GatewayService.SetActiveSandboxCount:input_type -> hypershell.v1.SetActiveSandboxCountRequest
+ 16, // 22: hypershell.v1.GatewayService.SetGatewayVersion:input_type -> hypershell.v1.SetGatewayVersionRequest
+ 9, // 23: hypershell.v1.GatewayService.GetGateway:output_type -> hypershell.v1.GetGatewayResponse
+ 7, // 24: hypershell.v1.GatewayService.CreateGateway:output_type -> hypershell.v1.CreateGatewayResponse
+ 11, // 25: hypershell.v1.GatewayService.UpdateGateway:output_type -> hypershell.v1.UpdateGatewayResponse
+ 21, // 26: hypershell.v1.GatewayService.DeleteGateway:output_type -> hypershell.v1.DeleteGatewayResponse
+ 20, // 27: hypershell.v1.GatewayService.ListGateways:output_type -> hypershell.v1.ListGatewaysResponse
+ 23, // 28: hypershell.v1.GatewayService.WatchGateways:output_type -> hypershell.v1.WatchGatewaysResponse
+ 13, // 29: hypershell.v1.GatewayService.AdjustActiveSandboxCount:output_type -> hypershell.v1.AdjustActiveSandboxCountResponse
+ 15, // 30: hypershell.v1.GatewayService.SetActiveSandboxCount:output_type -> hypershell.v1.SetActiveSandboxCountResponse
+ 17, // 31: hypershell.v1.GatewayService.SetGatewayVersion:output_type -> hypershell.v1.SetGatewayVersionResponse
+ 23, // [23:32] is the sub-list for method output_type
+ 14, // [14:23] is the sub-list for method input_type
+ 14, // [14:14] is the sub-list for extension type_name
+ 14, // [14:14] is the sub-list for extension extendee
+ 0, // [0:14] is the sub-list for field type_name
}
func init() { file_hypershell_v1_gateways_proto_init() }
@@ -1770,18 +1964,22 @@ func file_hypershell_v1_gateways_proto_init() {
return
}
file_hypershell_v1_common_proto_init()
- file_hypershell_v1_gateways_proto_msgTypes[1].OneofWrappers = []any{}
- file_hypershell_v1_gateways_proto_msgTypes[2].OneofWrappers = []any{}
- file_hypershell_v1_gateways_proto_msgTypes[6].OneofWrappers = []any{}
- file_hypershell_v1_gateways_proto_msgTypes[15].OneofWrappers = []any{}
+ file_hypershell_v1_gateways_proto_msgTypes[1].OneofWrappers = []any{
+ (*GatewayPlacementIntent_NetworkProvider)(nil),
+ (*GatewayPlacementIntent_LocalKind)(nil),
+ }
+ file_hypershell_v1_gateways_proto_msgTypes[4].OneofWrappers = []any{}
+ file_hypershell_v1_gateways_proto_msgTypes[5].OneofWrappers = []any{}
+ file_hypershell_v1_gateways_proto_msgTypes[9].OneofWrappers = []any{}
file_hypershell_v1_gateways_proto_msgTypes[18].OneofWrappers = []any{}
+ file_hypershell_v1_gateways_proto_msgTypes[21].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hypershell_v1_gateways_proto_rawDesc), len(file_hypershell_v1_gateways_proto_rawDesc)),
NumEnums: 1,
- NumMessages: 20,
+ NumMessages: 23,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/components/api-server/pkg/api/openapi/.openapi-generator/FILES b/components/api-server/pkg/api/openapi/.openapi-generator/FILES
index b33907ac0..fa0ab9bc7 100644
--- a/components/api-server/pkg/api/openapi/.openapi-generator/FILES
+++ b/components/api-server/pkg/api/openapi/.openapi-generator/FILES
@@ -16,6 +16,8 @@ docs/GatewayAllOfProvisioningConditions.md
docs/GatewayCreateRequest.md
docs/GatewayList.md
docs/GatewayPatchRequest.md
+docs/GatewayPlacementAvailability.md
+docs/GatewayPlacementIntent.md
docs/InferenceRoute.md
docs/InferenceRouteList.md
docs/List.md
@@ -66,6 +68,8 @@ model_gateway_all_of_provisioning_conditions.go
model_gateway_create_request.go
model_gateway_list.go
model_gateway_patch_request.go
+model_gateway_placement_availability.go
+model_gateway_placement_intent.go
model_inference_route.go
model_inference_route_list.go
model_list.go
diff --git a/components/api-server/pkg/api/openapi/README.md b/components/api-server/pkg/api/openapi/README.md
index c98e9b5e9..f157380f6 100644
--- a/components/api-server/pkg/api/openapi/README.md
+++ b/components/api-server/pkg/api/openapi/README.md
@@ -100,6 +100,7 @@ Class | Method | HTTP request | Description
*DefaultAPI* | [**DeleteSecretSource**](docs/DefaultAPI.md#deletesecretsource) | **Delete** /api/hypershell/ext/secret_sources/{id} | Delete a secret source
*DefaultAPI* | [**GetAgentRuntime**](docs/DefaultAPI.md#getagentruntime) | **Get** /api/hypershell/ext/agent_runtimes/{id} | Get an agent runtime by ID
*DefaultAPI* | [**GetGateway**](docs/DefaultAPI.md#getgateway) | **Get** /api/hypershell/v1/gateways/{id} | Get an gateway by id
+*DefaultAPI* | [**GetGatewayPlacementAvailability**](docs/DefaultAPI.md#getgatewayplacementavailability) | **Get** /api/hypershell/v1/gateways/placement-availability | Returns current provider placement eligibility
*DefaultAPI* | [**GetGatewayServiceAccount**](docs/DefaultAPI.md#getgatewayserviceaccount) | **Get** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id} | Get an OpenShell gateway service account
*DefaultAPI* | [**GetInferenceRoute**](docs/DefaultAPI.md#getinferenceroute) | **Get** /api/hypershell/ext/inference_routes/{id} | Get an inference route by ID
*DefaultAPI* | [**GetManagedCluster**](docs/DefaultAPI.md#getmanagedcluster) | **Get** /api/hypershell/v1/managed_clusters/{id} | Get an managedCluster by id
@@ -145,6 +146,8 @@ Class | Method | HTTP request | Description
- [GatewayCreateRequest](docs/GatewayCreateRequest.md)
- [GatewayList](docs/GatewayList.md)
- [GatewayPatchRequest](docs/GatewayPatchRequest.md)
+ - [GatewayPlacementAvailability](docs/GatewayPlacementAvailability.md)
+ - [GatewayPlacementIntent](docs/GatewayPlacementIntent.md)
- [InferenceRoute](docs/InferenceRoute.md)
- [InferenceRouteList](docs/InferenceRouteList.md)
- [List](docs/List.md)
diff --git a/components/api-server/pkg/api/openapi/api/openapi.yaml b/components/api-server/pkg/api/openapi/api/openapi.yaml
index c7a2ebdd4..9a249c7fe 100644
--- a/components/api-server/pkg/api/openapi/api/openapi.yaml
+++ b/components/api-server/pkg/api/openapi/api/openapi.yaml
@@ -474,9 +474,9 @@ paths:
schema:
$ref: "#/components/schemas/Error"
description: |
- Validation errors occurred, including a cluster_id that is empty or does not
- reference a ManagedCluster with a registered control plane (non-empty
- oidc_subject). On update, cluster_id is validated only when it changes.
+ Validation errors occurred, including an invalid or unavailable placement
+ intent. The backend resolves a valid intent to a registered ManagedCluster
+ before creating the gateway.
"401":
content:
application/json:
@@ -504,6 +504,31 @@ paths:
security:
- Bearer: []
summary: Create a new gateway
+ /api/hypershell/v1/gateways/placement-availability:
+ get:
+ operationId: getGatewayPlacementAvailability
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/GatewayPlacementAvailability"
+ description: Current backend placement eligibility
+ "401":
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ description: Auth token is invalid
+ "500":
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ description: Unexpected error evaluating placement availability
+ security:
+ - Bearer: []
+ summary: Returns current provider placement eligibility
/api/hypershell/v1/gateways/{id}:
delete:
operationId: deleteGateway
@@ -2826,6 +2851,15 @@ components:
name:
type: string
provider:
+ description: Cloud provider capability used for gateway placement.
+ type: string
+ visibility:
+ description: Network visibility supported by this control plane for gateway
+ placement.
+ enum:
+ - public
+ - vpn
+ readOnly: true
type: string
region:
type: string
@@ -2852,6 +2886,7 @@ components:
- provider
type: object
example:
+ visibility: public
kind: kind
created_at: 2000-01-23T04:56:07.000+00:00
oidc_subject: oidc_subject
@@ -2884,7 +2919,8 @@ components:
id: id
href: href
items:
- - kind: kind
+ - visibility: public
+ kind: kind
created_at: 2000-01-23T04:56:07.000+00:00
oidc_subject: oidc_subject
updated_at: 2000-01-23T04:56:07.000+00:00
@@ -2897,7 +2933,8 @@ components:
last_seen_at: 2000-01-23T04:56:07.000+00:00
status: status
api_server_url: api_server_url
- - kind: kind
+ - visibility: public
+ kind: kind
created_at: 2000-01-23T04:56:07.000+00:00
oidc_subject: oidc_subject
updated_at: 2000-01-23T04:56:07.000+00:00
@@ -2934,6 +2971,8 @@ components:
type: object
ManagedClusterRegistrationRequest:
example:
+ visibility: public
+ provider: aws
name: name
description: description
properties:
@@ -2944,8 +2983,23 @@ components:
description:
description: Optional description of the spoke.
type: string
+ provider:
+ description: Cloud provider hosting this control plane.
+ enum:
+ - aws
+ - ibm
+ - kind
+ type: string
+ visibility:
+ description: Network visibility supported for gateways on this control plane.
+ enum:
+ - public
+ - vpn
+ type: string
required:
- name
+ - provider
+ - visibility
type: object
ManagedClusterRegistrationResponse:
example:
@@ -3225,6 +3279,66 @@ components:
credential_driver:
type: string
type: object
+ GatewayPlacementIntent:
+ description: Exactly one of network/provider or mode must be supplied.
+ example:
+ mode: local-kind
+ provider: aws
+ network: public
+ nullable: true
+ oneOf: []
+ properties:
+ network:
+ enum:
+ - public
+ - vpn
+ type: string
+ provider:
+ description: IBM Cloud supports public placement only; VPN requires AWS.
+ enum:
+ - aws
+ - ibm
+ type: string
+ mode:
+ enum:
+ - local-kind
+ type: string
+ type: object
+ GatewayPlacementAvailability:
+ example:
+ aws_public: true
+ aws_reason: no-eligible-cluster
+ local_kind: true
+ ibm_reason: no-eligible-cluster
+ ibm_public: true
+ aws_vpn: true
+ ibm_vpn: true
+ properties:
+ aws_public:
+ type: boolean
+ aws_vpn:
+ type: boolean
+ ibm_public:
+ type: boolean
+ ibm_vpn:
+ type: boolean
+ local_kind:
+ type: boolean
+ aws_reason:
+ enum:
+ - no-eligible-cluster
+ type: string
+ ibm_reason:
+ enum:
+ - no-eligible-cluster
+ type: string
+ required:
+ - aws_public
+ - aws_vpn
+ - ibm_public
+ - ibm_vpn
+ - local_kind
+ type: object
OpenShellGatewayServiceAccountRole:
enum:
- openshell-user
@@ -4381,18 +4495,21 @@ components:
credential_driver: credential_driver
oidc: oidc
supervisor_image: supervisor_image
- cluster_id: cluster_id
service_type: service_type
tls_mode: tls_mode
route: route
name: name
+ placement:
+ mode: local-kind
+ provider: aws
+ network: public
external_dns: external_dns
status: status
properties:
name:
type: string
- cluster_id:
- type: string
+ placement:
+ $ref: "#/components/schemas/GatewayPlacementIntent"
external_dns:
type: string
tls_mode:
@@ -4424,8 +4541,8 @@ components:
description: JSON-encoded credential storage driver configuration
type: string
required:
- - cluster_id
- name
+ - placement
type: object
Gateway_allOf_provisioning_conditions:
example:
diff --git a/components/api-server/pkg/api/openapi/api_default.go b/components/api-server/pkg/api/openapi/api_default.go
index 29f832a78..aa7c563da 100644
--- a/components/api-server/pkg/api/openapi/api_default.go
+++ b/components/api-server/pkg/api/openapi/api_default.go
@@ -2894,6 +2894,125 @@ func (a *DefaultAPIService) GetGatewayExecute(r ApiGetGatewayRequest) (*Gateway,
return localVarReturnValue, localVarHTTPResponse, nil
}
+type ApiGetGatewayPlacementAvailabilityRequest struct {
+ ctx context.Context
+ ApiService *DefaultAPIService
+}
+
+func (r ApiGetGatewayPlacementAvailabilityRequest) Execute() (*GatewayPlacementAvailability, *http.Response, error) {
+ return r.ApiService.GetGatewayPlacementAvailabilityExecute(r)
+}
+
+/*
+GetGatewayPlacementAvailability Returns current provider placement eligibility
+
+ @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ @return ApiGetGatewayPlacementAvailabilityRequest
+*/
+func (a *DefaultAPIService) GetGatewayPlacementAvailability(ctx context.Context) ApiGetGatewayPlacementAvailabilityRequest {
+ return ApiGetGatewayPlacementAvailabilityRequest{
+ ApiService: a,
+ ctx: ctx,
+ }
+}
+
+// Execute executes the request
+//
+// @return GatewayPlacementAvailability
+func (a *DefaultAPIService) GetGatewayPlacementAvailabilityExecute(r ApiGetGatewayPlacementAvailabilityRequest) (*GatewayPlacementAvailability, *http.Response, error) {
+ var (
+ localVarHTTPMethod = http.MethodGet
+ localVarPostBody interface{}
+ formFiles []formFile
+ localVarReturnValue *GatewayPlacementAvailability
+ )
+
+ localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.GetGatewayPlacementAvailability")
+ if err != nil {
+ return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
+ }
+
+ localVarPath := localBasePath + "/api/hypershell/v1/gateways/placement-availability"
+
+ localVarHeaderParams := make(map[string]string)
+ localVarQueryParams := url.Values{}
+ localVarFormParams := url.Values{}
+
+ // to determine the Content-Type header
+ localVarHTTPContentTypes := []string{}
+
+ // set Content-Type header
+ localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+ if localVarHTTPContentType != "" {
+ localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+ }
+
+ // to determine the Accept header
+ localVarHTTPHeaderAccepts := []string{"application/json"}
+
+ // set Accept header
+ localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+ if localVarHTTPHeaderAccept != "" {
+ localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+ }
+ req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
+ if err != nil {
+ return localVarReturnValue, nil, err
+ }
+
+ localVarHTTPResponse, err := a.client.callAPI(req)
+ if err != nil || localVarHTTPResponse == nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
+ localVarHTTPResponse.Body.Close()
+ localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
+ if err != nil {
+ return localVarReturnValue, localVarHTTPResponse, err
+ }
+
+ if localVarHTTPResponse.StatusCode >= 300 {
+ newErr := &GenericOpenAPIError{
+ body: localVarBody,
+ error: localVarHTTPResponse.Status,
+ }
+ if localVarHTTPResponse.StatusCode == 401 {
+ var v Error
+ err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
+ newErr.model = v
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ if localVarHTTPResponse.StatusCode == 500 {
+ var v Error
+ err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr.error = err.Error()
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+ newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
+ newErr.model = v
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+ if err != nil {
+ newErr := &GenericOpenAPIError{
+ body: localVarBody,
+ error: err.Error(),
+ }
+ return localVarReturnValue, localVarHTTPResponse, newErr
+ }
+
+ return localVarReturnValue, localVarHTTPResponse, nil
+}
+
type ApiGetGatewayServiceAccountRequest struct {
ctx context.Context
ApiService *DefaultAPIService
diff --git a/components/api-server/pkg/api/openapi/docs/DefaultAPI.md b/components/api-server/pkg/api/openapi/docs/DefaultAPI.md
index c10a0bccd..025c0d1c7 100644
--- a/components/api-server/pkg/api/openapi/docs/DefaultAPI.md
+++ b/components/api-server/pkg/api/openapi/docs/DefaultAPI.md
@@ -26,6 +26,7 @@ Method | HTTP request | Description
[**DeleteSecretSource**](DefaultAPI.md#DeleteSecretSource) | **Delete** /api/hypershell/ext/secret_sources/{id} | Delete a secret source
[**GetAgentRuntime**](DefaultAPI.md#GetAgentRuntime) | **Get** /api/hypershell/ext/agent_runtimes/{id} | Get an agent runtime by ID
[**GetGateway**](DefaultAPI.md#GetGateway) | **Get** /api/hypershell/v1/gateways/{id} | Get an gateway by id
+[**GetGatewayPlacementAvailability**](DefaultAPI.md#GetGatewayPlacementAvailability) | **Get** /api/hypershell/v1/gateways/placement-availability | Returns current provider placement eligibility
[**GetGatewayServiceAccount**](DefaultAPI.md#GetGatewayServiceAccount) | **Get** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id} | Get an OpenShell gateway service account
[**GetInferenceRoute**](DefaultAPI.md#GetInferenceRoute) | **Get** /api/hypershell/ext/inference_routes/{id} | Get an inference route by ID
[**GetManagedCluster**](DefaultAPI.md#GetManagedCluster) | **Get** /api/hypershell/v1/managed_clusters/{id} | Get an managedCluster by id
@@ -144,7 +145,7 @@ import (
)
func main() {
- gatewayCreateRequest := *openapiclient.NewGatewayCreateRequest("Name_example", "ClusterId_example") // GatewayCreateRequest | Gateway data
+ gatewayCreateRequest := *openapiclient.NewGatewayCreateRequest("Name_example", "TODO") // GatewayCreateRequest | Gateway data
configuration := openapiclient.NewConfiguration()
apiClient := openapiclient.NewAPIClient(configuration)
@@ -1508,6 +1509,65 @@ Name | Type | Description | Notes
[[Back to README]](../README.md)
+## GetGatewayPlacementAvailability
+
+> GatewayPlacementAvailability GetGatewayPlacementAvailability(ctx).Execute()
+
+Returns current provider placement eligibility
+
+### Example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID"
+)
+
+func main() {
+
+ configuration := openapiclient.NewConfiguration()
+ apiClient := openapiclient.NewAPIClient(configuration)
+ resp, r, err := apiClient.DefaultAPI.GetGatewayPlacementAvailability(context.Background()).Execute()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.GetGatewayPlacementAvailability``: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
+ }
+ // response from `GetGatewayPlacementAvailability`: GatewayPlacementAvailability
+ fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.GetGatewayPlacementAvailability`: %v\n", resp)
+}
+```
+
+### Path Parameters
+
+This endpoint does not need any parameter.
+
+### Other Parameters
+
+Other parameters are passed through a pointer to a apiGetGatewayPlacementAvailabilityRequest struct via the builder pattern
+
+
+### Return type
+
+[**GatewayPlacementAvailability**](GatewayPlacementAvailability.md)
+
+### Authorization
+
+[Bearer](../README.md#Bearer)
+
+### HTTP request headers
+
+- **Content-Type**: Not defined
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
## GetGatewayServiceAccount
> OpenShellGatewayServiceAccountGetResponse GetGatewayServiceAccount(ctx, gatewayId, serviceAccountId).Execute()
@@ -3143,7 +3203,7 @@ import (
)
func main() {
- managedClusterRegistrationRequest := *openapiclient.NewManagedClusterRegistrationRequest("Name_example") // ManagedClusterRegistrationRequest | Registration request
+ managedClusterRegistrationRequest := *openapiclient.NewManagedClusterRegistrationRequest("Name_example", "Provider_example", "Visibility_example") // ManagedClusterRegistrationRequest | Registration request
configuration := openapiclient.NewConfiguration()
apiClient := openapiclient.NewAPIClient(configuration)
diff --git a/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md b/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md
index 3a2a17300..33e981f18 100644
--- a/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md
+++ b/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md
@@ -5,7 +5,7 @@
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**Name** | **string** | |
-**ClusterId** | **string** | |
+**Placement** | [**NullableGatewayPlacementIntent**](GatewayPlacementIntent.md) | |
**ExternalDns** | Pointer to **string** | | [optional]
**TlsMode** | Pointer to **string** | | [optional]
**ServiceType** | Pointer to **string** | | [optional]
@@ -22,7 +22,7 @@ Name | Type | Description | Notes
### NewGatewayCreateRequest
-`func NewGatewayCreateRequest(name string, clusterId string, ) *GatewayCreateRequest`
+`func NewGatewayCreateRequest(name string, placement NullableGatewayPlacementIntent, ) *GatewayCreateRequest`
NewGatewayCreateRequest instantiates a new GatewayCreateRequest object
This constructor will assign default values to properties that have it defined,
@@ -57,26 +57,36 @@ and a boolean to check if the value has been set.
SetName sets Name field to given value.
-### GetClusterId
+### GetPlacement
-`func (o *GatewayCreateRequest) GetClusterId() string`
+`func (o *GatewayCreateRequest) GetPlacement() GatewayPlacementIntent`
-GetClusterId returns the ClusterId field if non-nil, zero value otherwise.
+GetPlacement returns the Placement field if non-nil, zero value otherwise.
-### GetClusterIdOk
+### GetPlacementOk
-`func (o *GatewayCreateRequest) GetClusterIdOk() (*string, bool)`
+`func (o *GatewayCreateRequest) GetPlacementOk() (*GatewayPlacementIntent, bool)`
-GetClusterIdOk returns a tuple with the ClusterId field if it's non-nil, zero value otherwise
+GetPlacementOk returns a tuple with the Placement field if it's non-nil, zero value otherwise
and a boolean to check if the value has been set.
-### SetClusterId
+### SetPlacement
-`func (o *GatewayCreateRequest) SetClusterId(v string)`
+`func (o *GatewayCreateRequest) SetPlacement(v GatewayPlacementIntent)`
-SetClusterId sets ClusterId field to given value.
+SetPlacement sets Placement field to given value.
+### SetPlacementNil
+
+`func (o *GatewayCreateRequest) SetPlacementNil(b bool)`
+
+ SetPlacementNil sets the value for Placement to be an explicit nil
+
+### UnsetPlacement
+`func (o *GatewayCreateRequest) UnsetPlacement()`
+
+UnsetPlacement ensures that no value is present for Placement, not even an explicit nil
### GetExternalDns
`func (o *GatewayCreateRequest) GetExternalDns() string`
diff --git a/components/api-server/pkg/api/openapi/docs/GatewayPlacementAvailability.md b/components/api-server/pkg/api/openapi/docs/GatewayPlacementAvailability.md
new file mode 100644
index 000000000..386edab2e
--- /dev/null
+++ b/components/api-server/pkg/api/openapi/docs/GatewayPlacementAvailability.md
@@ -0,0 +1,187 @@
+# GatewayPlacementAvailability
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**AwsPublic** | **bool** | |
+**AwsVpn** | **bool** | |
+**IbmPublic** | **bool** | |
+**IbmVpn** | **bool** | |
+**LocalKind** | **bool** | |
+**AwsReason** | Pointer to **string** | | [optional]
+**IbmReason** | Pointer to **string** | | [optional]
+
+## Methods
+
+### NewGatewayPlacementAvailability
+
+`func NewGatewayPlacementAvailability(awsPublic bool, awsVpn bool, ibmPublic bool, ibmVpn bool, localKind bool, ) *GatewayPlacementAvailability`
+
+NewGatewayPlacementAvailability instantiates a new GatewayPlacementAvailability object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewGatewayPlacementAvailabilityWithDefaults
+
+`func NewGatewayPlacementAvailabilityWithDefaults() *GatewayPlacementAvailability`
+
+NewGatewayPlacementAvailabilityWithDefaults instantiates a new GatewayPlacementAvailability object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetAwsPublic
+
+`func (o *GatewayPlacementAvailability) GetAwsPublic() bool`
+
+GetAwsPublic returns the AwsPublic field if non-nil, zero value otherwise.
+
+### GetAwsPublicOk
+
+`func (o *GatewayPlacementAvailability) GetAwsPublicOk() (*bool, bool)`
+
+GetAwsPublicOk returns a tuple with the AwsPublic field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetAwsPublic
+
+`func (o *GatewayPlacementAvailability) SetAwsPublic(v bool)`
+
+SetAwsPublic sets AwsPublic field to given value.
+
+
+### GetAwsVpn
+
+`func (o *GatewayPlacementAvailability) GetAwsVpn() bool`
+
+GetAwsVpn returns the AwsVpn field if non-nil, zero value otherwise.
+
+### GetAwsVpnOk
+
+`func (o *GatewayPlacementAvailability) GetAwsVpnOk() (*bool, bool)`
+
+GetAwsVpnOk returns a tuple with the AwsVpn field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetAwsVpn
+
+`func (o *GatewayPlacementAvailability) SetAwsVpn(v bool)`
+
+SetAwsVpn sets AwsVpn field to given value.
+
+
+### GetIbmPublic
+
+`func (o *GatewayPlacementAvailability) GetIbmPublic() bool`
+
+GetIbmPublic returns the IbmPublic field if non-nil, zero value otherwise.
+
+### GetIbmPublicOk
+
+`func (o *GatewayPlacementAvailability) GetIbmPublicOk() (*bool, bool)`
+
+GetIbmPublicOk returns a tuple with the IbmPublic field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetIbmPublic
+
+`func (o *GatewayPlacementAvailability) SetIbmPublic(v bool)`
+
+SetIbmPublic sets IbmPublic field to given value.
+
+
+### GetIbmVpn
+
+`func (o *GatewayPlacementAvailability) GetIbmVpn() bool`
+
+GetIbmVpn returns the IbmVpn field if non-nil, zero value otherwise.
+
+### GetIbmVpnOk
+
+`func (o *GatewayPlacementAvailability) GetIbmVpnOk() (*bool, bool)`
+
+GetIbmVpnOk returns a tuple with the IbmVpn field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetIbmVpn
+
+`func (o *GatewayPlacementAvailability) SetIbmVpn(v bool)`
+
+SetIbmVpn sets IbmVpn field to given value.
+
+
+### GetLocalKind
+
+`func (o *GatewayPlacementAvailability) GetLocalKind() bool`
+
+GetLocalKind returns the LocalKind field if non-nil, zero value otherwise.
+
+### GetLocalKindOk
+
+`func (o *GatewayPlacementAvailability) GetLocalKindOk() (*bool, bool)`
+
+GetLocalKindOk returns a tuple with the LocalKind field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetLocalKind
+
+`func (o *GatewayPlacementAvailability) SetLocalKind(v bool)`
+
+SetLocalKind sets LocalKind field to given value.
+
+
+### GetAwsReason
+
+`func (o *GatewayPlacementAvailability) GetAwsReason() string`
+
+GetAwsReason returns the AwsReason field if non-nil, zero value otherwise.
+
+### GetAwsReasonOk
+
+`func (o *GatewayPlacementAvailability) GetAwsReasonOk() (*string, bool)`
+
+GetAwsReasonOk returns a tuple with the AwsReason field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetAwsReason
+
+`func (o *GatewayPlacementAvailability) SetAwsReason(v string)`
+
+SetAwsReason sets AwsReason field to given value.
+
+### HasAwsReason
+
+`func (o *GatewayPlacementAvailability) HasAwsReason() bool`
+
+HasAwsReason returns a boolean if a field has been set.
+
+### GetIbmReason
+
+`func (o *GatewayPlacementAvailability) GetIbmReason() string`
+
+GetIbmReason returns the IbmReason field if non-nil, zero value otherwise.
+
+### GetIbmReasonOk
+
+`func (o *GatewayPlacementAvailability) GetIbmReasonOk() (*string, bool)`
+
+GetIbmReasonOk returns a tuple with the IbmReason field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetIbmReason
+
+`func (o *GatewayPlacementAvailability) SetIbmReason(v string)`
+
+SetIbmReason sets IbmReason field to given value.
+
+### HasIbmReason
+
+`func (o *GatewayPlacementAvailability) HasIbmReason() bool`
+
+HasIbmReason returns a boolean if a field has been set.
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/components/api-server/pkg/api/openapi/docs/GatewayPlacementIntent.md b/components/api-server/pkg/api/openapi/docs/GatewayPlacementIntent.md
new file mode 100644
index 000000000..7889310df
--- /dev/null
+++ b/components/api-server/pkg/api/openapi/docs/GatewayPlacementIntent.md
@@ -0,0 +1,108 @@
+# GatewayPlacementIntent
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**Network** | Pointer to **string** | | [optional]
+**Provider** | Pointer to **string** | IBM Cloud supports public placement only; VPN requires AWS. | [optional]
+**Mode** | Pointer to **string** | | [optional]
+
+## Methods
+
+### NewGatewayPlacementIntent
+
+`func NewGatewayPlacementIntent() *GatewayPlacementIntent`
+
+NewGatewayPlacementIntent instantiates a new GatewayPlacementIntent object
+This constructor will assign default values to properties that have it defined,
+and makes sure properties required by API are set, but the set of arguments
+will change when the set of required properties is changed
+
+### NewGatewayPlacementIntentWithDefaults
+
+`func NewGatewayPlacementIntentWithDefaults() *GatewayPlacementIntent`
+
+NewGatewayPlacementIntentWithDefaults instantiates a new GatewayPlacementIntent object
+This constructor will only assign default values to properties that have it defined,
+but it doesn't guarantee that properties required by API are set
+
+### GetNetwork
+
+`func (o *GatewayPlacementIntent) GetNetwork() string`
+
+GetNetwork returns the Network field if non-nil, zero value otherwise.
+
+### GetNetworkOk
+
+`func (o *GatewayPlacementIntent) GetNetworkOk() (*string, bool)`
+
+GetNetworkOk returns a tuple with the Network field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetNetwork
+
+`func (o *GatewayPlacementIntent) SetNetwork(v string)`
+
+SetNetwork sets Network field to given value.
+
+### HasNetwork
+
+`func (o *GatewayPlacementIntent) HasNetwork() bool`
+
+HasNetwork returns a boolean if a field has been set.
+
+### GetProvider
+
+`func (o *GatewayPlacementIntent) GetProvider() string`
+
+GetProvider returns the Provider field if non-nil, zero value otherwise.
+
+### GetProviderOk
+
+`func (o *GatewayPlacementIntent) GetProviderOk() (*string, bool)`
+
+GetProviderOk returns a tuple with the Provider field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetProvider
+
+`func (o *GatewayPlacementIntent) SetProvider(v string)`
+
+SetProvider sets Provider field to given value.
+
+### HasProvider
+
+`func (o *GatewayPlacementIntent) HasProvider() bool`
+
+HasProvider returns a boolean if a field has been set.
+
+### GetMode
+
+`func (o *GatewayPlacementIntent) GetMode() string`
+
+GetMode returns the Mode field if non-nil, zero value otherwise.
+
+### GetModeOk
+
+`func (o *GatewayPlacementIntent) GetModeOk() (*string, bool)`
+
+GetModeOk returns a tuple with the Mode field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetMode
+
+`func (o *GatewayPlacementIntent) SetMode(v string)`
+
+SetMode sets Mode field to given value.
+
+### HasMode
+
+`func (o *GatewayPlacementIntent) HasMode() bool`
+
+HasMode returns a boolean if a field has been set.
+
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/components/api-server/pkg/api/openapi/docs/ManagedCluster.md b/components/api-server/pkg/api/openapi/docs/ManagedCluster.md
index 8799634fc..09d1479ba 100644
--- a/components/api-server/pkg/api/openapi/docs/ManagedCluster.md
+++ b/components/api-server/pkg/api/openapi/docs/ManagedCluster.md
@@ -10,7 +10,8 @@ Name | Type | Description | Notes
**CreatedAt** | Pointer to **time.Time** | | [optional]
**UpdatedAt** | Pointer to **time.Time** | | [optional]
**Name** | **string** | |
-**Provider** | **string** | |
+**Provider** | **string** | Cloud provider capability used for gateway placement. |
+**Visibility** | Pointer to **string** | Network visibility supported by this control plane for gateway placement. | [optional] [readonly]
**Region** | Pointer to **string** | | [optional]
**KubeconfigSecret** | **string** | |
**Status** | Pointer to **string** | | [optional]
@@ -202,6 +203,31 @@ and a boolean to check if the value has been set.
SetProvider sets Provider field to given value.
+### GetVisibility
+
+`func (o *ManagedCluster) GetVisibility() string`
+
+GetVisibility returns the Visibility field if non-nil, zero value otherwise.
+
+### GetVisibilityOk
+
+`func (o *ManagedCluster) GetVisibilityOk() (*string, bool)`
+
+GetVisibilityOk returns a tuple with the Visibility field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetVisibility
+
+`func (o *ManagedCluster) SetVisibility(v string)`
+
+SetVisibility sets Visibility field to given value.
+
+### HasVisibility
+
+`func (o *ManagedCluster) HasVisibility() bool`
+
+HasVisibility returns a boolean if a field has been set.
+
### GetRegion
`func (o *ManagedCluster) GetRegion() string`
diff --git a/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md b/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md
index 7706e10e0..117db5917 100644
--- a/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md
+++ b/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md
@@ -6,12 +6,14 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**Name** | **string** | Human-readable spoke name, unique per fleet (e.g. hyp0-mc1). Must match on every subsequent call. |
**Description** | Pointer to **string** | Optional description of the spoke. | [optional]
+**Provider** | **string** | Cloud provider hosting this control plane. |
+**Visibility** | **string** | Network visibility supported for gateways on this control plane. |
## Methods
### NewManagedClusterRegistrationRequest
-`func NewManagedClusterRegistrationRequest(name string, ) *ManagedClusterRegistrationRequest`
+`func NewManagedClusterRegistrationRequest(name string, provider string, visibility string, ) *ManagedClusterRegistrationRequest`
NewManagedClusterRegistrationRequest instantiates a new ManagedClusterRegistrationRequest object
This constructor will assign default values to properties that have it defined,
@@ -71,6 +73,46 @@ SetDescription sets Description field to given value.
HasDescription returns a boolean if a field has been set.
+### GetProvider
+
+`func (o *ManagedClusterRegistrationRequest) GetProvider() string`
+
+GetProvider returns the Provider field if non-nil, zero value otherwise.
+
+### GetProviderOk
+
+`func (o *ManagedClusterRegistrationRequest) GetProviderOk() (*string, bool)`
+
+GetProviderOk returns a tuple with the Provider field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetProvider
+
+`func (o *ManagedClusterRegistrationRequest) SetProvider(v string)`
+
+SetProvider sets Provider field to given value.
+
+
+### GetVisibility
+
+`func (o *ManagedClusterRegistrationRequest) GetVisibility() string`
+
+GetVisibility returns the Visibility field if non-nil, zero value otherwise.
+
+### GetVisibilityOk
+
+`func (o *ManagedClusterRegistrationRequest) GetVisibilityOk() (*string, bool)`
+
+GetVisibilityOk returns a tuple with the Visibility field if it's non-nil, zero value otherwise
+and a boolean to check if the value has been set.
+
+### SetVisibility
+
+`func (o *ManagedClusterRegistrationRequest) SetVisibility(v string)`
+
+SetVisibility sets Visibility field to given value.
+
+
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
diff --git a/components/api-server/pkg/api/openapi/model_gateway_create_request.go b/components/api-server/pkg/api/openapi/model_gateway_create_request.go
index bab7ebcab..452d01010 100644
--- a/components/api-server/pkg/api/openapi/model_gateway_create_request.go
+++ b/components/api-server/pkg/api/openapi/model_gateway_create_request.go
@@ -21,13 +21,13 @@ var _ MappedNullable = &GatewayCreateRequest{}
// GatewayCreateRequest struct for GatewayCreateRequest
type GatewayCreateRequest struct {
- Name string `json:"name"`
- ClusterId string `json:"cluster_id"`
- ExternalDns *string `json:"external_dns,omitempty"`
- TlsMode *string `json:"tls_mode,omitempty"`
- ServiceType *string `json:"service_type,omitempty"`
- Status *string `json:"status,omitempty"`
- Phase *string `json:"phase,omitempty"`
+ Name string `json:"name"`
+ Placement NullableGatewayPlacementIntent `json:"placement"`
+ ExternalDns *string `json:"external_dns,omitempty"`
+ TlsMode *string `json:"tls_mode,omitempty"`
+ ServiceType *string `json:"service_type,omitempty"`
+ Status *string `json:"status,omitempty"`
+ Phase *string `json:"phase,omitempty"`
// Container image for the gateway deployment
Image *string `json:"image,omitempty"`
// Container image for the supervisor sidecar
@@ -48,10 +48,10 @@ type _GatewayCreateRequest GatewayCreateRequest
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
-func NewGatewayCreateRequest(name string, clusterId string) *GatewayCreateRequest {
+func NewGatewayCreateRequest(name string, placement NullableGatewayPlacementIntent) *GatewayCreateRequest {
this := GatewayCreateRequest{}
this.Name = name
- this.ClusterId = clusterId
+ this.Placement = placement
return &this
}
@@ -87,28 +87,30 @@ func (o *GatewayCreateRequest) SetName(v string) {
o.Name = v
}
-// GetClusterId returns the ClusterId field value
-func (o *GatewayCreateRequest) GetClusterId() string {
- if o == nil {
- var ret string
+// GetPlacement returns the Placement field value
+// If the value is explicit nil, the zero value for GatewayPlacementIntent will be returned
+func (o *GatewayCreateRequest) GetPlacement() GatewayPlacementIntent {
+ if o == nil || o.Placement.Get() == nil {
+ var ret GatewayPlacementIntent
return ret
}
- return o.ClusterId
+ return *o.Placement.Get()
}
-// GetClusterIdOk returns a tuple with the ClusterId field value
+// GetPlacementOk returns a tuple with the Placement field value
// and a boolean to check if the value has been set.
-func (o *GatewayCreateRequest) GetClusterIdOk() (*string, bool) {
+// NOTE: If the value is an explicit nil, `nil, true` will be returned
+func (o *GatewayCreateRequest) GetPlacementOk() (*GatewayPlacementIntent, bool) {
if o == nil {
return nil, false
}
- return &o.ClusterId, true
+ return o.Placement.Get(), o.Placement.IsSet()
}
-// SetClusterId sets field value
-func (o *GatewayCreateRequest) SetClusterId(v string) {
- o.ClusterId = v
+// SetPlacement sets field value
+func (o *GatewayCreateRequest) SetPlacement(v GatewayPlacementIntent) {
+ o.Placement.Set(&v)
}
// GetExternalDns returns the ExternalDns field value if set, zero value otherwise.
@@ -474,7 +476,7 @@ func (o GatewayCreateRequest) MarshalJSON() ([]byte, error) {
func (o GatewayCreateRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["name"] = o.Name
- toSerialize["cluster_id"] = o.ClusterId
+ toSerialize["placement"] = o.Placement.Get()
if !IsNil(o.ExternalDns) {
toSerialize["external_dns"] = o.ExternalDns
}
@@ -517,7 +519,7 @@ func (o *GatewayCreateRequest) UnmarshalJSON(data []byte) (err error) {
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"name",
- "cluster_id",
+ "placement",
}
allProperties := make(map[string]interface{})
diff --git a/components/api-server/pkg/api/openapi/model_gateway_placement_availability.go b/components/api-server/pkg/api/openapi/model_gateway_placement_availability.go
new file mode 100644
index 000000000..56e041210
--- /dev/null
+++ b/components/api-server/pkg/api/openapi/model_gateway_placement_availability.go
@@ -0,0 +1,340 @@
+/*
+HyperShell API
+
+HyperShell gateway management API
+
+API version: 1.0.0
+*/
+
+// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
+
+package openapi
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
+// checks if the GatewayPlacementAvailability type satisfies the MappedNullable interface at compile time
+var _ MappedNullable = &GatewayPlacementAvailability{}
+
+// GatewayPlacementAvailability struct for GatewayPlacementAvailability
+type GatewayPlacementAvailability struct {
+ AwsPublic bool `json:"aws_public"`
+ AwsVpn bool `json:"aws_vpn"`
+ IbmPublic bool `json:"ibm_public"`
+ IbmVpn bool `json:"ibm_vpn"`
+ LocalKind bool `json:"local_kind"`
+ AwsReason *string `json:"aws_reason,omitempty"`
+ IbmReason *string `json:"ibm_reason,omitempty"`
+}
+
+type _GatewayPlacementAvailability GatewayPlacementAvailability
+
+// NewGatewayPlacementAvailability instantiates a new GatewayPlacementAvailability object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewGatewayPlacementAvailability(awsPublic bool, awsVpn bool, ibmPublic bool, ibmVpn bool, localKind bool) *GatewayPlacementAvailability {
+ this := GatewayPlacementAvailability{}
+ this.AwsPublic = awsPublic
+ this.AwsVpn = awsVpn
+ this.IbmPublic = ibmPublic
+ this.IbmVpn = ibmVpn
+ this.LocalKind = localKind
+ return &this
+}
+
+// NewGatewayPlacementAvailabilityWithDefaults instantiates a new GatewayPlacementAvailability object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewGatewayPlacementAvailabilityWithDefaults() *GatewayPlacementAvailability {
+ this := GatewayPlacementAvailability{}
+ return &this
+}
+
+// GetAwsPublic returns the AwsPublic field value
+func (o *GatewayPlacementAvailability) GetAwsPublic() bool {
+ if o == nil {
+ var ret bool
+ return ret
+ }
+
+ return o.AwsPublic
+}
+
+// GetAwsPublicOk returns a tuple with the AwsPublic field value
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetAwsPublicOk() (*bool, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.AwsPublic, true
+}
+
+// SetAwsPublic sets field value
+func (o *GatewayPlacementAvailability) SetAwsPublic(v bool) {
+ o.AwsPublic = v
+}
+
+// GetAwsVpn returns the AwsVpn field value
+func (o *GatewayPlacementAvailability) GetAwsVpn() bool {
+ if o == nil {
+ var ret bool
+ return ret
+ }
+
+ return o.AwsVpn
+}
+
+// GetAwsVpnOk returns a tuple with the AwsVpn field value
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetAwsVpnOk() (*bool, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.AwsVpn, true
+}
+
+// SetAwsVpn sets field value
+func (o *GatewayPlacementAvailability) SetAwsVpn(v bool) {
+ o.AwsVpn = v
+}
+
+// GetIbmPublic returns the IbmPublic field value
+func (o *GatewayPlacementAvailability) GetIbmPublic() bool {
+ if o == nil {
+ var ret bool
+ return ret
+ }
+
+ return o.IbmPublic
+}
+
+// GetIbmPublicOk returns a tuple with the IbmPublic field value
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetIbmPublicOk() (*bool, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.IbmPublic, true
+}
+
+// SetIbmPublic sets field value
+func (o *GatewayPlacementAvailability) SetIbmPublic(v bool) {
+ o.IbmPublic = v
+}
+
+// GetIbmVpn returns the IbmVpn field value
+func (o *GatewayPlacementAvailability) GetIbmVpn() bool {
+ if o == nil {
+ var ret bool
+ return ret
+ }
+
+ return o.IbmVpn
+}
+
+// GetIbmVpnOk returns a tuple with the IbmVpn field value
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetIbmVpnOk() (*bool, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.IbmVpn, true
+}
+
+// SetIbmVpn sets field value
+func (o *GatewayPlacementAvailability) SetIbmVpn(v bool) {
+ o.IbmVpn = v
+}
+
+// GetLocalKind returns the LocalKind field value
+func (o *GatewayPlacementAvailability) GetLocalKind() bool {
+ if o == nil {
+ var ret bool
+ return ret
+ }
+
+ return o.LocalKind
+}
+
+// GetLocalKindOk returns a tuple with the LocalKind field value
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetLocalKindOk() (*bool, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.LocalKind, true
+}
+
+// SetLocalKind sets field value
+func (o *GatewayPlacementAvailability) SetLocalKind(v bool) {
+ o.LocalKind = v
+}
+
+// GetAwsReason returns the AwsReason field value if set, zero value otherwise.
+func (o *GatewayPlacementAvailability) GetAwsReason() string {
+ if o == nil || IsNil(o.AwsReason) {
+ var ret string
+ return ret
+ }
+ return *o.AwsReason
+}
+
+// GetAwsReasonOk returns a tuple with the AwsReason field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetAwsReasonOk() (*string, bool) {
+ if o == nil || IsNil(o.AwsReason) {
+ return nil, false
+ }
+ return o.AwsReason, true
+}
+
+// HasAwsReason returns a boolean if a field has been set.
+func (o *GatewayPlacementAvailability) HasAwsReason() bool {
+ if o != nil && !IsNil(o.AwsReason) {
+ return true
+ }
+
+ return false
+}
+
+// SetAwsReason gets a reference to the given string and assigns it to the AwsReason field.
+func (o *GatewayPlacementAvailability) SetAwsReason(v string) {
+ o.AwsReason = &v
+}
+
+// GetIbmReason returns the IbmReason field value if set, zero value otherwise.
+func (o *GatewayPlacementAvailability) GetIbmReason() string {
+ if o == nil || IsNil(o.IbmReason) {
+ var ret string
+ return ret
+ }
+ return *o.IbmReason
+}
+
+// GetIbmReasonOk returns a tuple with the IbmReason field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementAvailability) GetIbmReasonOk() (*string, bool) {
+ if o == nil || IsNil(o.IbmReason) {
+ return nil, false
+ }
+ return o.IbmReason, true
+}
+
+// HasIbmReason returns a boolean if a field has been set.
+func (o *GatewayPlacementAvailability) HasIbmReason() bool {
+ if o != nil && !IsNil(o.IbmReason) {
+ return true
+ }
+
+ return false
+}
+
+// SetIbmReason gets a reference to the given string and assigns it to the IbmReason field.
+func (o *GatewayPlacementAvailability) SetIbmReason(v string) {
+ o.IbmReason = &v
+}
+
+func (o GatewayPlacementAvailability) MarshalJSON() ([]byte, error) {
+ toSerialize, err := o.ToMap()
+ if err != nil {
+ return []byte{}, err
+ }
+ return json.Marshal(toSerialize)
+}
+
+func (o GatewayPlacementAvailability) ToMap() (map[string]interface{}, error) {
+ toSerialize := map[string]interface{}{}
+ toSerialize["aws_public"] = o.AwsPublic
+ toSerialize["aws_vpn"] = o.AwsVpn
+ toSerialize["ibm_public"] = o.IbmPublic
+ toSerialize["ibm_vpn"] = o.IbmVpn
+ toSerialize["local_kind"] = o.LocalKind
+ if !IsNil(o.AwsReason) {
+ toSerialize["aws_reason"] = o.AwsReason
+ }
+ if !IsNil(o.IbmReason) {
+ toSerialize["ibm_reason"] = o.IbmReason
+ }
+ return toSerialize, nil
+}
+
+func (o *GatewayPlacementAvailability) UnmarshalJSON(data []byte) (err error) {
+ // This validates that all required properties are included in the JSON object
+ // by unmarshalling the object into a generic map with string keys and checking
+ // that every required field exists as a key in the generic map.
+ requiredProperties := []string{
+ "aws_public",
+ "aws_vpn",
+ "ibm_public",
+ "ibm_vpn",
+ "local_kind",
+ }
+
+ allProperties := make(map[string]interface{})
+
+ err = json.Unmarshal(data, &allProperties)
+
+ if err != nil {
+ return err
+ }
+
+ for _, requiredProperty := range requiredProperties {
+ if _, exists := allProperties[requiredProperty]; !exists {
+ return fmt.Errorf("no value given for required property %v", requiredProperty)
+ }
+ }
+
+ varGatewayPlacementAvailability := _GatewayPlacementAvailability{}
+
+ decoder := json.NewDecoder(bytes.NewReader(data))
+ decoder.DisallowUnknownFields()
+ err = decoder.Decode(&varGatewayPlacementAvailability)
+
+ if err != nil {
+ return err
+ }
+
+ *o = GatewayPlacementAvailability(varGatewayPlacementAvailability)
+
+ return err
+}
+
+type NullableGatewayPlacementAvailability struct {
+ value *GatewayPlacementAvailability
+ isSet bool
+}
+
+func (v NullableGatewayPlacementAvailability) Get() *GatewayPlacementAvailability {
+ return v.value
+}
+
+func (v *NullableGatewayPlacementAvailability) Set(val *GatewayPlacementAvailability) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableGatewayPlacementAvailability) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableGatewayPlacementAvailability) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableGatewayPlacementAvailability(val *GatewayPlacementAvailability) *NullableGatewayPlacementAvailability {
+ return &NullableGatewayPlacementAvailability{value: val, isSet: true}
+}
+
+func (v NullableGatewayPlacementAvailability) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableGatewayPlacementAvailability) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/components/api-server/pkg/api/openapi/model_gateway_placement_intent.go b/components/api-server/pkg/api/openapi/model_gateway_placement_intent.go
new file mode 100644
index 000000000..bf38526b8
--- /dev/null
+++ b/components/api-server/pkg/api/openapi/model_gateway_placement_intent.go
@@ -0,0 +1,197 @@
+/*
+HyperShell API
+
+HyperShell gateway management API
+
+API version: 1.0.0
+*/
+
+// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
+
+package openapi
+
+import (
+ "encoding/json"
+)
+
+// checks if the GatewayPlacementIntent type satisfies the MappedNullable interface at compile time
+var _ MappedNullable = &GatewayPlacementIntent{}
+
+// GatewayPlacementIntent Exactly one of network/provider or mode must be supplied.
+type GatewayPlacementIntent struct {
+ Network *string
+ // IBM Cloud supports public placement only; VPN requires AWS.
+ Provider *string
+ Mode *string
+}
+
+// NewGatewayPlacementIntent instantiates a new GatewayPlacementIntent object
+// This constructor will assign default values to properties that have it defined,
+// and makes sure properties required by API are set, but the set of arguments
+// will change when the set of required properties is changed
+func NewGatewayPlacementIntent() *GatewayPlacementIntent {
+ this := GatewayPlacementIntent{}
+ return &this
+}
+
+// NewGatewayPlacementIntentWithDefaults instantiates a new GatewayPlacementIntent object
+// This constructor will only assign default values to properties that have it defined,
+// but it doesn't guarantee that properties required by API are set
+func NewGatewayPlacementIntentWithDefaults() *GatewayPlacementIntent {
+ this := GatewayPlacementIntent{}
+ return &this
+}
+
+// GetNetwork returns the Network field value if set, zero value otherwise.
+func (o *GatewayPlacementIntent) GetNetwork() string {
+ if o == nil || IsNil(o.Network) {
+ var ret string
+ return ret
+ }
+ return *o.Network
+}
+
+// GetNetworkOk returns a tuple with the Network field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementIntent) GetNetworkOk() (*string, bool) {
+ if o == nil || IsNil(o.Network) {
+ return nil, false
+ }
+ return o.Network, true
+}
+
+// HasNetwork returns a boolean if a field has been set.
+func (o *GatewayPlacementIntent) HasNetwork() bool {
+ if o != nil && !IsNil(o.Network) {
+ return true
+ }
+
+ return false
+}
+
+// SetNetwork gets a reference to the given string and assigns it to the Network field.
+func (o *GatewayPlacementIntent) SetNetwork(v string) {
+ o.Network = &v
+}
+
+// GetProvider returns the Provider field value if set, zero value otherwise.
+func (o *GatewayPlacementIntent) GetProvider() string {
+ if o == nil || IsNil(o.Provider) {
+ var ret string
+ return ret
+ }
+ return *o.Provider
+}
+
+// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementIntent) GetProviderOk() (*string, bool) {
+ if o == nil || IsNil(o.Provider) {
+ return nil, false
+ }
+ return o.Provider, true
+}
+
+// HasProvider returns a boolean if a field has been set.
+func (o *GatewayPlacementIntent) HasProvider() bool {
+ if o != nil && !IsNil(o.Provider) {
+ return true
+ }
+
+ return false
+}
+
+// SetProvider gets a reference to the given string and assigns it to the Provider field.
+func (o *GatewayPlacementIntent) SetProvider(v string) {
+ o.Provider = &v
+}
+
+// GetMode returns the Mode field value if set, zero value otherwise.
+func (o *GatewayPlacementIntent) GetMode() string {
+ if o == nil || IsNil(o.Mode) {
+ var ret string
+ return ret
+ }
+ return *o.Mode
+}
+
+// GetModeOk returns a tuple with the Mode field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *GatewayPlacementIntent) GetModeOk() (*string, bool) {
+ if o == nil || IsNil(o.Mode) {
+ return nil, false
+ }
+ return o.Mode, true
+}
+
+// HasMode returns a boolean if a field has been set.
+func (o *GatewayPlacementIntent) HasMode() bool {
+ if o != nil && !IsNil(o.Mode) {
+ return true
+ }
+
+ return false
+}
+
+// SetMode gets a reference to the given string and assigns it to the Mode field.
+func (o *GatewayPlacementIntent) SetMode(v string) {
+ o.Mode = &v
+}
+
+func (o GatewayPlacementIntent) MarshalJSON() ([]byte, error) {
+ toSerialize, err := o.ToMap()
+ if err != nil {
+ return []byte{}, err
+ }
+ return json.Marshal(toSerialize)
+}
+
+func (o GatewayPlacementIntent) ToMap() (map[string]interface{}, error) {
+ toSerialize := map[string]interface{}{}
+ if !IsNil(o.Network) {
+ toSerialize["network"] = o.Network
+ }
+ if !IsNil(o.Provider) {
+ toSerialize["provider"] = o.Provider
+ }
+ if !IsNil(o.Mode) {
+ toSerialize["mode"] = o.Mode
+ }
+ return toSerialize, nil
+}
+
+type NullableGatewayPlacementIntent struct {
+ value *GatewayPlacementIntent
+ isSet bool
+}
+
+func (v NullableGatewayPlacementIntent) Get() *GatewayPlacementIntent {
+ return v.value
+}
+
+func (v *NullableGatewayPlacementIntent) Set(val *GatewayPlacementIntent) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableGatewayPlacementIntent) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableGatewayPlacementIntent) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableGatewayPlacementIntent(val *GatewayPlacementIntent) *NullableGatewayPlacementIntent {
+ return &NullableGatewayPlacementIntent{value: val, isSet: true}
+}
+
+func (v NullableGatewayPlacementIntent) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableGatewayPlacementIntent) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/components/api-server/pkg/api/openapi/model_managed_cluster.go b/components/api-server/pkg/api/openapi/model_managed_cluster.go
index aba2ca4a4..a4d6b3acd 100644
--- a/components/api-server/pkg/api/openapi/model_managed_cluster.go
+++ b/components/api-server/pkg/api/openapi/model_managed_cluster.go
@@ -22,17 +22,20 @@ var _ MappedNullable = &ManagedCluster{}
// ManagedCluster struct for ManagedCluster
type ManagedCluster struct {
- Id *string `json:"id,omitempty"`
- Kind *string `json:"kind,omitempty"`
- Href *string `json:"href,omitempty"`
- CreatedAt *time.Time `json:"created_at,omitempty"`
- UpdatedAt *time.Time `json:"updated_at,omitempty"`
- Name string `json:"name"`
- Provider string `json:"provider"`
- Region *string `json:"region,omitempty"`
- KubeconfigSecret string `json:"kubeconfig_secret"`
- Status *string `json:"status,omitempty"`
- ApiServerUrl *string `json:"api_server_url,omitempty"`
+ Id *string `json:"id,omitempty"`
+ Kind *string `json:"kind,omitempty"`
+ Href *string `json:"href,omitempty"`
+ CreatedAt *time.Time `json:"created_at,omitempty"`
+ UpdatedAt *time.Time `json:"updated_at,omitempty"`
+ Name string `json:"name"`
+ // Cloud provider capability used for gateway placement.
+ Provider string `json:"provider"`
+ // Network visibility supported by this control plane for gateway placement.
+ Visibility *string `json:"visibility,omitempty"`
+ Region *string `json:"region,omitempty"`
+ KubeconfigSecret string `json:"kubeconfig_secret"`
+ Status *string `json:"status,omitempty"`
+ ApiServerUrl *string `json:"api_server_url,omitempty"`
// OIDC sub claim of the service account that registered this cluster. Server-assigned; not writable.
OidcSubject *string `json:"oidc_subject,omitempty"`
// Timestamp of the most recent registration call. Updated on every POST /registration.
@@ -269,6 +272,38 @@ func (o *ManagedCluster) SetProvider(v string) {
o.Provider = v
}
+// GetVisibility returns the Visibility field value if set, zero value otherwise.
+func (o *ManagedCluster) GetVisibility() string {
+ if o == nil || IsNil(o.Visibility) {
+ var ret string
+ return ret
+ }
+ return *o.Visibility
+}
+
+// GetVisibilityOk returns a tuple with the Visibility field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *ManagedCluster) GetVisibilityOk() (*string, bool) {
+ if o == nil || IsNil(o.Visibility) {
+ return nil, false
+ }
+ return o.Visibility, true
+}
+
+// HasVisibility returns a boolean if a field has been set.
+func (o *ManagedCluster) HasVisibility() bool {
+ if o != nil && !IsNil(o.Visibility) {
+ return true
+ }
+
+ return false
+}
+
+// SetVisibility gets a reference to the given string and assigns it to the Visibility field.
+func (o *ManagedCluster) SetVisibility(v string) {
+ o.Visibility = &v
+}
+
// GetRegion returns the Region field value if set, zero value otherwise.
func (o *ManagedCluster) GetRegion() string {
if o == nil || IsNil(o.Region) {
@@ -480,6 +515,9 @@ func (o ManagedCluster) ToMap() (map[string]interface{}, error) {
}
toSerialize["name"] = o.Name
toSerialize["provider"] = o.Provider
+ if !IsNil(o.Visibility) {
+ toSerialize["visibility"] = o.Visibility
+ }
if !IsNil(o.Region) {
toSerialize["region"] = o.Region
}
diff --git a/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go b/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go
index 0ef47e4a2..aa24983a3 100644
--- a/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go
+++ b/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go
@@ -25,6 +25,10 @@ type ManagedClusterRegistrationRequest struct {
Name string `json:"name"`
// Optional description of the spoke.
Description *string `json:"description,omitempty"`
+ // Cloud provider hosting this control plane.
+ Provider string `json:"provider"`
+ // Network visibility supported for gateways on this control plane.
+ Visibility string `json:"visibility"`
}
type _ManagedClusterRegistrationRequest ManagedClusterRegistrationRequest
@@ -33,9 +37,11 @@ type _ManagedClusterRegistrationRequest ManagedClusterRegistrationRequest
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
-func NewManagedClusterRegistrationRequest(name string) *ManagedClusterRegistrationRequest {
+func NewManagedClusterRegistrationRequest(name string, provider string, visibility string) *ManagedClusterRegistrationRequest {
this := ManagedClusterRegistrationRequest{}
this.Name = name
+ this.Provider = provider
+ this.Visibility = visibility
return &this
}
@@ -103,6 +109,54 @@ func (o *ManagedClusterRegistrationRequest) SetDescription(v string) {
o.Description = &v
}
+// GetProvider returns the Provider field value
+func (o *ManagedClusterRegistrationRequest) GetProvider() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Provider
+}
+
+// GetProviderOk returns a tuple with the Provider field value
+// and a boolean to check if the value has been set.
+func (o *ManagedClusterRegistrationRequest) GetProviderOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Provider, true
+}
+
+// SetProvider sets field value
+func (o *ManagedClusterRegistrationRequest) SetProvider(v string) {
+ o.Provider = v
+}
+
+// GetVisibility returns the Visibility field value
+func (o *ManagedClusterRegistrationRequest) GetVisibility() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Visibility
+}
+
+// GetVisibilityOk returns a tuple with the Visibility field value
+// and a boolean to check if the value has been set.
+func (o *ManagedClusterRegistrationRequest) GetVisibilityOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Visibility, true
+}
+
+// SetVisibility sets field value
+func (o *ManagedClusterRegistrationRequest) SetVisibility(v string) {
+ o.Visibility = v
+}
+
func (o ManagedClusterRegistrationRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
@@ -117,6 +171,8 @@ func (o ManagedClusterRegistrationRequest) ToMap() (map[string]interface{}, erro
if !IsNil(o.Description) {
toSerialize["description"] = o.Description
}
+ toSerialize["provider"] = o.Provider
+ toSerialize["visibility"] = o.Visibility
return toSerialize, nil
}
@@ -126,6 +182,8 @@ func (o *ManagedClusterRegistrationRequest) UnmarshalJSON(data []byte) (err erro
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"name",
+ "provider",
+ "visibility",
}
allProperties := make(map[string]interface{})
diff --git a/components/api-server/plugins/gateways/availability_handler_test.go b/components/api-server/plugins/gateways/availability_handler_test.go
new file mode 100644
index 000000000..af5b5a43d
--- /dev/null
+++ b/components/api-server/plugins/gateways/availability_handler_test.go
@@ -0,0 +1,31 @@
+package gateways
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
+ "github.com/openshift-online/rh-trex-ai/components/api-server/pkg/errors"
+)
+
+func TestGetPlacementAvailabilityAcceptsEmptyGetBody(t *testing.T) {
+ handler := gatewayHandler{
+ availability: func(context.Context) (openapi.GatewayPlacementAvailability, *errors.ServiceError) {
+ return openapi.GatewayPlacementAvailability{LocalKind: true}, nil
+ },
+ }
+
+ request := httptest.NewRequest(http.MethodGet, "/gateways/placement-availability", nil)
+ response := httptest.NewRecorder()
+ handler.GetPlacementAvailability(response, request)
+
+ if response.Code != http.StatusOK {
+ t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
+ }
+ if !strings.Contains(response.Body.String(), `"local_kind":true`) {
+ t.Fatalf("response body = %s", response.Body.String())
+ }
+}
diff --git a/components/api-server/plugins/gateways/cluster_fixture_test.go b/components/api-server/plugins/gateways/cluster_fixture_test.go
index 9573f87d7..d0aa87bf1 100644
--- a/components/api-server/plugins/gateways/cluster_fixture_test.go
+++ b/components/api-server/plugins/gateways/cluster_fixture_test.go
@@ -8,6 +8,7 @@ import (
. "github.com/onsi/gomega"
+ "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
"github.com/openshift-online/hypershell/components/api-server/plugins/managedClusters"
"github.com/openshift-online/hypershell/components/api-server/test"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/api"
@@ -16,14 +17,23 @@ import (
// registerTestCluster registers a ManagedCluster the way a control plane does
// (a unique OIDC subject via POST /registration semantics) and returns its id.
-// Gateway create/update handlers accept only a cluster_id that references a
-// registered cluster (managed-cluster-registration.spec.md).
+// Placement resolution and gateway update handlers use only registered
+// clusters (managed-cluster-registration.spec.md).
func registerTestCluster(t *testing.T) string {
t.Helper()
id, _ := registerTestClusterWithSubject(t)
return id
}
+func managedGatewayCreateRequest(name string) openapi.GatewayCreateRequest {
+ request := openapi.GatewayCreateRequest{Name: name}
+ request.SetPlacement(openapi.GatewayPlacementIntent{
+ Network: openapi.PtrString("public"),
+ Provider: openapi.PtrString("aws"),
+ })
+ return request
+}
+
// registerTestClusterWithSubject is registerTestCluster that also returns the
// OIDC subject the cluster registered under, so a test can act as its control
// plane.
@@ -40,7 +50,7 @@ func mustRegisterCluster() (string, string) {
subject := "cp-" + suffix
name := fmt.Sprintf("mc-%s", suffix)
svc := managedClusters.Service(&environments.Environment().Services)
- cluster, _, svcErr := svc.Register(context.Background(), name, "", subject)
+ cluster, _, svcErr := svc.Register(context.Background(), name, "", "aws", "public", subject)
Expect(svcErr).To(BeNil(), "register test managed cluster %s", name)
return cluster.ID, subject
}
diff --git a/components/api-server/plugins/gateways/cluster_reference.go b/components/api-server/plugins/gateways/cluster_reference.go
index 6902797c3..d49b4c0c9 100644
--- a/components/api-server/plugins/gateways/cluster_reference.go
+++ b/components/api-server/plugins/gateways/cluster_reference.go
@@ -4,9 +4,42 @@ import (
"context"
"strings"
+ "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/errors"
)
+type PlacementResolver func(context.Context, openapi.GatewayPlacementIntent) (string, *errors.ServiceError)
+type PlacementAvailabilityResolver func(context.Context) (openapi.GatewayPlacementAvailability, *errors.ServiceError)
+
+func validatePlacementIntent(intent openapi.GatewayPlacementIntent) *errors.ServiceError {
+ mode, network, provider := "", "", ""
+ if intent.Mode != nil {
+ mode = *intent.Mode
+ }
+ if intent.Network != nil {
+ network = *intent.Network
+ }
+ if intent.Provider != nil {
+ provider = *intent.Provider
+ }
+ if mode != "" {
+ if mode != "local-kind" || network != "" || provider != "" {
+ return errors.Validation("placement mode must be local-kind and cannot include network or provider")
+ }
+ return nil
+ }
+ if network != "public" && network != "vpn" {
+ return errors.Validation("placement.network must be public or vpn")
+ }
+ if provider != "aws" && provider != "ibm" {
+ return errors.Validation("placement.provider must be aws or ibm")
+ }
+ if network == "vpn" && provider != "aws" {
+ return errors.Validation("VPN placement requires provider aws")
+ }
+ return nil
+}
+
// RegisteredClusterLookup reports whether id names a ManagedCluster that a
// control plane registered (non-empty oidc_subject). It returns (false, nil) for
// an unknown id or a manually created record, and an error only when the lookup
diff --git a/components/api-server/plugins/gateways/grpc_handler.go b/components/api-server/plugins/gateways/grpc_handler.go
index 1730d3c67..8136b86ab 100644
--- a/components/api-server/plugins/gateways/grpc_handler.go
+++ b/components/api-server/plugins/gateways/grpc_handler.go
@@ -11,6 +11,7 @@ import (
"google.golang.org/grpc/status"
pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1"
+ "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
"github.com/openshift-online/hypershell/components/api-server/pkg/gatewayhealth"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/api"
pkgserver "github.com/openshift-online/rh-trex-ai/components/api-server/pkg/server"
@@ -24,10 +25,15 @@ type gatewayGRPCHandler struct {
generic services.GenericService
brokerFunc func() *pkgserver.EventBroker
clusters RegisteredClusterLookup
+ placement PlacementResolver
}
-func NewGatewayGRPCHandler(svc GatewayService, generic services.GenericService, brokerFunc func() *pkgserver.EventBroker, clusters RegisteredClusterLookup) pb.GatewayServiceServer {
- return &gatewayGRPCHandler{service: svc, generic: generic, brokerFunc: brokerFunc, clusters: clusters}
+func NewGatewayGRPCHandler(svc GatewayService, generic services.GenericService, brokerFunc func() *pkgserver.EventBroker, clusters RegisteredClusterLookup, placement ...PlacementResolver) pb.GatewayServiceServer {
+ h := &gatewayGRPCHandler{service: svc, generic: generic, brokerFunc: brokerFunc, clusters: clusters}
+ if len(placement) > 0 {
+ h.placement = placement[0]
+ }
+ return h
}
// validateGatewayPhase rejects a phase outside the canonical vocabulary. An
@@ -58,13 +64,37 @@ func (h *gatewayGRPCHandler) CreateGateway(ctx context.Context, req *pb.CreateGa
if err := grpcutil.ValidateStringField("name", req.Name, true); err != nil {
return nil, err
}
- if err := grpcutil.ValidateStringField("cluster_id", req.ClusterId, true); err != nil {
+ if err := validateGatewayPhase(req.Phase); err != nil {
return nil, err
}
- if err := validateGatewayPhase(req.Phase); err != nil {
+ clusterID := req.ClusterId
+ if req.Placement != nil {
+ if h.placement == nil {
+ return nil, status.Error(codes.FailedPrecondition, "gateway placement resolver is unavailable")
+ }
+ intent := openapi.GatewayPlacementIntent{}
+ if network := req.Placement.GetNetworkProvider(); network != nil {
+ intent.Network = &network.Network
+ intent.Provider = &network.Provider
+ } else if req.Placement.GetLocalKind() != nil {
+ mode := "local-kind"
+ intent.Mode = &mode
+ } else {
+ return nil, status.Error(codes.InvalidArgument, "placement intent is empty")
+ }
+ if svcErr := validatePlacementIntent(intent); svcErr != nil {
+ return nil, grpcutil.ServiceErrorToGRPC(svcErr)
+ }
+ resolved, svcErr := h.placement(ctx, intent)
+ if svcErr != nil {
+ return nil, grpcutil.ServiceErrorToGRPC(svcErr)
+ }
+ clusterID = resolved
+ }
+ if err := grpcutil.ValidateStringField("cluster_id", clusterID, true); err != nil {
return nil, err
}
- if svcErr := validateClusterReference(ctx, h.clusters, req.ClusterId); svcErr != nil {
+ if svcErr := validateClusterReference(ctx, h.clusters, clusterID); svcErr != nil {
return nil, grpcutil.ServiceErrorToGRPC(svcErr)
}
var serverDnsNamesJSON *string
@@ -76,7 +106,7 @@ func (h *gatewayGRPCHandler) CreateGateway(ctx context.Context, req *pb.CreateGa
gateway := &Gateway{
Name: req.Name,
- ClusterId: req.ClusterId,
+ ClusterId: clusterID,
ExternalDns: req.ExternalDns,
TlsMode: req.TlsMode,
ServiceType: req.ServiceType,
diff --git a/components/api-server/plugins/gateways/grpc_integration_test.go b/components/api-server/plugins/gateways/grpc_integration_test.go
index 8d0334b52..f27c9d775 100644
--- a/components/api-server/plugins/gateways/grpc_integration_test.go
+++ b/components/api-server/plugins/gateways/grpc_integration_test.go
@@ -13,7 +13,6 @@ import (
"google.golang.org/grpc/credentials/insecure"
pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1"
- "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
"github.com/openshift-online/hypershell/components/api-server/test"
)
@@ -151,7 +150,7 @@ func TestGRPCWatchGateways(t *testing.T) {
wg.Add(2)
sinkReady := make(chan struct{})
- watchClusterID := registerTestCluster(t)
+ registerTestCluster(t)
go func() {
defer wg.Done()
@@ -159,10 +158,7 @@ func TestGRPCWatchGateways(t *testing.T) {
time.Sleep(100 * time.Millisecond)
for name := range itemNames {
- gatewayInput := openapi.GatewayCreateRequest{
- Name: name,
- ClusterId: watchClusterID,
- }
+ gatewayInput := managedGatewayCreateRequest(name)
_, resp, postErr := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
if postErr != nil {
sourceErr = fmt.Errorf("REST POST failed for %s: %v", name, postErr)
diff --git a/components/api-server/plugins/gateways/handler.go b/components/api-server/plugins/gateways/handler.go
index 8b1e2a71c..2f6b2d439 100644
--- a/components/api-server/plugins/gateways/handler.go
+++ b/components/api-server/plugins/gateways/handler.go
@@ -40,6 +40,24 @@ type gatewayHandler struct {
visibilityFilter GatewayVisibilityFilter
ownerLookup GatewayOwnerLookup
clusters RegisteredClusterLookup
+ placement PlacementResolver
+ availability PlacementAvailabilityResolver
+}
+
+func (h gatewayHandler) GetPlacementAvailability(w http.ResponseWriter, r *http.Request) {
+ if h.availability == nil {
+ handlers.HandleGet(w, r, &handlers.HandlerConfig{Action: func() (interface{}, *errors.ServiceError) {
+ return nil, errors.GeneralError("gateway placement resolver is unavailable")
+ }, ErrorHandler: handlers.HandleError})
+ return
+ }
+ handlers.HandleGet(w, r, &handlers.HandlerConfig{Action: func() (interface{}, *errors.ServiceError) {
+ availability, svcErr := h.availability(r.Context())
+ if svcErr != nil {
+ return nil, svcErr
+ }
+ return availability, nil
+ }, ErrorHandler: handlers.HandleError})
}
// validateGatewayPhaseValue rejects a phase outside the canonical vocabulary. An
@@ -54,15 +72,18 @@ func validateGatewayPhaseValue(phase *string) *errors.ServiceError {
return nil
}
-func NewGatewayHandler(gateway GatewayService, generic services.GenericService, ownerBinding OwnerBindingCreator, visibilityFilter GatewayVisibilityFilter, ownerLookup GatewayOwnerLookup, clusters RegisteredClusterLookup) *gatewayHandler {
- return &gatewayHandler{
+func NewGatewayHandler(gateway GatewayService, generic services.GenericService, ownerBinding OwnerBindingCreator, visibilityFilter GatewayVisibilityFilter, ownerLookup GatewayOwnerLookup, clusters RegisteredClusterLookup, placement PlacementResolver, availability PlacementAvailabilityResolver) *gatewayHandler {
+ h := &gatewayHandler{
gateway: gateway,
generic: generic,
ownerBinding: ownerBinding,
visibilityFilter: visibilityFilter,
ownerLookup: ownerLookup,
clusters: clusters,
+ placement: placement,
+ availability: availability,
}
+ return h
}
func (h gatewayHandler) Create(w http.ResponseWriter, r *http.Request) {
@@ -73,6 +94,18 @@ func (h gatewayHandler) Create(w http.ResponseWriter, r *http.Request) {
Action: func() (interface{}, *errors.ServiceError) {
ctx := r.Context()
gatewayModel := ConvertGateway(gateway)
+ placement := gateway.GetPlacement()
+ if placementErr := validatePlacementIntent(placement); placementErr != nil {
+ return nil, placementErr
+ }
+ if h.placement == nil {
+ return nil, errors.GeneralError("gateway placement resolver is unavailable")
+ }
+ clusterID, placementErr := h.placement(ctx, placement)
+ if placementErr != nil {
+ return nil, placementErr
+ }
+ gatewayModel.ClusterId = clusterID
if phaseErr := validateGatewayPhaseValue(gatewayModel.Phase); phaseErr != nil {
return nil, phaseErr
}
diff --git a/components/api-server/plugins/gateways/integration_test.go b/components/api-server/plugins/gateways/integration_test.go
index e7d9dccee..b3b338d61 100644
--- a/components/api-server/plugins/gateways/integration_test.go
+++ b/components/api-server/plugins/gateways/integration_test.go
@@ -50,15 +50,13 @@ func TestGatewayPost(t *testing.T) {
account := h.NewRandAccount()
ctx := h.NewAuthenticatedContext(account)
- gatewayInput := openapi.GatewayCreateRequest{
- Name: "test-name",
- ClusterId: registerTestCluster(t),
- ExternalDns: openapi.PtrString("test-external_dns"),
- TlsMode: openapi.PtrString("test-tls_mode"),
- ServiceType: openapi.PtrString("test-service_type"),
- Status: openapi.PtrString("test-status"),
- Phase: openapi.PtrString("Provisioning"),
- }
+ clusterID := registerTestCluster(t)
+ gatewayInput := managedGatewayCreateRequest("test-name")
+ gatewayInput.ExternalDns = openapi.PtrString("test-external_dns")
+ gatewayInput.TlsMode = openapi.PtrString("test-tls_mode")
+ gatewayInput.ServiceType = openapi.PtrString("test-service_type")
+ gatewayInput.Status = openapi.PtrString("test-status")
+ gatewayInput.Phase = openapi.PtrString("Provisioning")
gatewayOutput, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).NotTo(HaveOccurred(), "Error posting object: %v", err)
@@ -66,6 +64,7 @@ func TestGatewayPost(t *testing.T) {
Expect(*gatewayOutput.Id).NotTo(BeEmpty(), "Expected ID assigned on creation")
Expect(*gatewayOutput.Kind).To(Equal("Gateway"))
Expect(*gatewayOutput.Href).To(Equal(fmt.Sprintf("/api/hypershell/v1/gateways/%s", *gatewayOutput.Id)))
+ Expect(gatewayOutput.ClusterId).To(Equal(clusterID))
Expect(gatewayOutput.Namespace).To(MatchRegexp(`^openshell-[0-9a-f]{16}$`))
jwtToken := ctx.Value(openapi.ContextAccessToken)
@@ -85,10 +84,7 @@ func TestGatewayPostMinimalFields(t *testing.T) {
account := h.NewRandAccount()
ctx := h.NewAuthenticatedContext(account)
clusterID := registerTestCluster(t)
- gatewayInput := openapi.GatewayCreateRequest{
- Name: "local-gateway",
- ClusterId: clusterID,
- }
+ gatewayInput := managedGatewayCreateRequest("local-gateway")
gatewayOutput, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).NotTo(HaveOccurred(), "Error posting gateway with minimal fields: %v", err)
@@ -97,43 +93,24 @@ func TestGatewayPostMinimalFields(t *testing.T) {
Expect(gatewayOutput.Namespace).To(MatchRegexp(`^openshell-[0-9a-f]{16}$`))
}
-// Gateways Reference a Registered Cluster: an empty cluster_id is a 400 naming
-// cluster_id, and no gateway is created.
-func TestGatewayPostRejectsEmptyClusterID(t *testing.T) {
+// Gateway creation accepts placement intent only. A legacy direct cluster_id
+// is rejected as an unknown request field even when valid placement is present.
+func TestGatewayPostRejectsDirectClusterID(t *testing.T) {
h, client := test.RegisterIntegration(t)
account := h.NewRandAccount()
ctx := h.NewAuthenticatedContext(account)
-
- _, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: "no-cluster",
- ClusterId: "",
- }).Execute()
- Expect(err).To(HaveOccurred())
- Expect(resp.StatusCode).To(Equal(http.StatusBadRequest))
- Expect(openapiErrorReason(h, err)).To(ContainSubstring("cluster_id"))
- expectNoGatewayNamed(ctx, client, "no-cluster")
-}
-
-// Gateways Reference a Registered Cluster: a cluster_id naming a manually
-// created ManagedCluster (empty oidc_subject) or no ManagedCluster at all is a
-// 400 stating the cluster has no registered control plane.
-func TestGatewayPostRejectsUnregisteredCluster(t *testing.T) {
- h, client := test.RegisterIntegration(t)
-
- account := h.NewRandAccount()
- ctx := h.NewAuthenticatedContext(account)
-
- for _, clusterID := range []string{createManualCluster(t), "2doesnotexist000000000000000"} {
- _, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: "unregistered-cluster",
- ClusterId: clusterID,
- }).Execute()
- Expect(err).To(HaveOccurred(), "cluster_id %s must be rejected", clusterID)
- Expect(resp.StatusCode).To(Equal(http.StatusBadRequest))
- Expect(openapiErrorReason(h, err)).To(ContainSubstring("registered control plane"))
- }
- expectNoGatewayNamed(ctx, client, "unregistered-cluster")
+ registerTestCluster(t)
+ jwToken := ctx.Value(openapi.ContextAccessToken)
+ resp, err := resty.R().
+ SetHeader("Content-Type", "application/json").
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", jwToken)).
+ SetBody(`{"name":"direct-cluster","placement":{"network":"public","provider":"aws"},"cluster_id":"legacy-cluster"}`).
+ Post(h.RestURL("/gateways"))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode()).To(Equal(http.StatusBadRequest))
+ Expect(string(resp.Body())).To(ContainSubstring("cluster_id"))
+ expectNoGatewayNamed(ctx, client, "direct-cluster")
}
// A PATCH that moves a gateway to an unregistered cluster is rejected; one that
@@ -144,11 +121,8 @@ func TestGatewayPatchValidatesClusterReassignment(t *testing.T) {
account := h.NewRandAccount()
ctx := h.NewAuthenticatedContext(account)
- clusterID := registerTestCluster(t)
- created, _, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: "patch-cluster",
- ClusterId: clusterID,
- }).Execute()
+ registerTestCluster(t)
+ created, _, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(managedGatewayCreateRequest("patch-cluster")).Execute()
Expect(err).NotTo(HaveOccurred())
_, resp, err := client.DefaultAPI.UpdateGateway(ctx, *created.Id).GatewayPatchRequest(openapi.GatewayPatchRequest{
@@ -195,10 +169,8 @@ func TestGatewayPostWithoutRouteRemainsUnrouted(t *testing.T) {
account := h.NewRandAccount()
ctx := h.NewAuthenticatedContext(account)
- gatewayInput := openapi.GatewayCreateRequest{
- Name: "route-default-test",
- ClusterId: registerTestCluster(t),
- }
+ registerTestCluster(t)
+ gatewayInput := managedGatewayCreateRequest("route-default-test")
gatewayOutput, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).NotTo(HaveOccurred())
@@ -213,11 +185,9 @@ func TestGatewayPostPreservesExplicitRoute(t *testing.T) {
ctx := h.NewAuthenticatedContext(account)
customRoute := `{"enabled":true,"host":"custom.example.com"}`
- gatewayInput := openapi.GatewayCreateRequest{
- Name: "route-explicit-test",
- ClusterId: registerTestCluster(t),
- Route: openapi.PtrString(customRoute),
- }
+ registerTestCluster(t)
+ gatewayInput := managedGatewayCreateRequest("route-explicit-test")
+ gatewayInput.Route = openapi.PtrString(customRoute)
gatewayOutput, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).NotTo(HaveOccurred())
@@ -332,11 +302,9 @@ func TestGatewayPostWithCredentialDriver(t *testing.T) {
ctx := h.NewAuthenticatedContext(account)
credDriver := `{"type":"kubernetes-secrets","kubernetes_secrets":{"namespace":"creds-ns"}}`
- gatewayInput := openapi.GatewayCreateRequest{
- Name: "test-cred-driver",
- ClusterId: registerTestCluster(t),
- CredentialDriver: openapi.PtrString(credDriver),
- }
+ registerTestCluster(t)
+ gatewayInput := managedGatewayCreateRequest("test-cred-driver")
+ gatewayInput.CredentialDriver = openapi.PtrString(credDriver)
gatewayOutput, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).NotTo(HaveOccurred(), "Error posting gateway with credential_driver: %v", err)
diff --git a/components/api-server/plugins/gateways/phase_validation_test.go b/components/api-server/plugins/gateways/phase_validation_test.go
index 1d1ef86fc..9a79a3075 100644
--- a/components/api-server/plugins/gateways/phase_validation_test.go
+++ b/components/api-server/plugins/gateways/phase_validation_test.go
@@ -43,6 +43,7 @@ func TestGRPCGatewayRejectsUnknownPhase(t *testing.T) {
_, err = grpcClient.CreateGateway(ctx, &pb.CreateGatewayRequest{
Name: "reject-create",
ClusterId: registerTestCluster(t),
+ ReleaseId: "test-release_id",
Phase: &badPhase,
})
Expect(err).To(HaveOccurred(), "create with an unknown phase must be rejected")
@@ -53,6 +54,7 @@ func TestGRPCGatewayRejectsUnknownPhase(t *testing.T) {
created, err := grpcClient.CreateGateway(ctx, &pb.CreateGatewayRequest{
Name: "accept-create",
ClusterId: registerTestCluster(t),
+ ReleaseId: "test-release_id",
Phase: &goodPhase,
})
Expect(err).NotTo(HaveOccurred())
@@ -82,6 +84,7 @@ func TestGRPCGatewayRejectsUnknownPhase(t *testing.T) {
_, err = grpcClient.CreateGateway(ctx, &pb.CreateGatewayRequest{
Name: "accept-absent-phase",
ClusterId: registerTestCluster(t),
+ ReleaseId: "test-release_id",
})
Expect(err).NotTo(HaveOccurred(), "create without a phase must be accepted")
}
@@ -120,11 +123,10 @@ func TestRESTGatewayRejectsUnknownPhase(t *testing.T) {
account := h.NewRandAccount()
ctx := h.NewAuthenticatedContext(account)
- _, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: "reject-rest-create",
- ClusterId: registerTestCluster(t),
- Phase: openapi.PtrString("Booting"),
- }).Execute()
+ registerTestCluster(t)
+ request := managedGatewayCreateRequest("reject-rest-create")
+ request.Phase = openapi.PtrString("Booting")
+ _, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(request).Execute()
Expect(err).To(HaveOccurred(), "REST create with an unknown phase must be rejected")
Expect(resp.StatusCode).To(Equal(http.StatusBadRequest))
}
diff --git a/components/api-server/plugins/gateways/placement.go b/components/api-server/plugins/gateways/placement.go
new file mode 100644
index 000000000..97b4ac60c
--- /dev/null
+++ b/components/api-server/plugins/gateways/placement.go
@@ -0,0 +1,84 @@
+package gateways
+
+import (
+ "crypto/rand"
+ "fmt"
+ "math/big"
+ "time"
+)
+
+// PlacementIntent is the client-facing placement request. Cluster IDs are
+// deliberately absent: they are selected by the API server at create time.
+type PlacementIntent struct {
+ Mode string `json:"mode,omitempty"`
+ Network string `json:"network,omitempty"`
+ Provider string `json:"provider,omitempty"`
+}
+
+const managedClusterConnectedMaxAge = 5 * time.Minute
+
+func PlacementControlPlaneConnected(lastSeen *time.Time, now time.Time) bool {
+ if lastSeen == nil {
+ return false
+ }
+ age := now.Sub(*lastSeen)
+ return age >= 0 && age < managedClusterConnectedMaxAge
+}
+
+type PlacementCandidate struct {
+ ID string
+ Provider string
+ Visibility string
+ Connected bool
+}
+
+func PlacementSupported(network, provider string) bool {
+ switch provider {
+ case "aws":
+ return network == "public" || network == "vpn"
+ case "ibm":
+ return network == "public"
+ default:
+ return false
+ }
+}
+
+func eligiblePlacementCandidates(intent PlacementIntent, candidates []PlacementCandidate) []PlacementCandidate {
+ eligible := make([]PlacementCandidate, 0, len(candidates))
+ for _, candidate := range candidates {
+ if candidate.Connected && candidate.Provider == intent.Provider && candidate.Visibility == intent.Network {
+ eligible = append(eligible, candidate)
+ }
+ }
+ return eligible
+}
+
+func randomPlacementIndex(size int) (int, error) {
+ index, err := rand.Int(rand.Reader, big.NewInt(int64(size)))
+ if err != nil {
+ return 0, fmt.Errorf("select random placement: %w", err)
+ }
+ return int(index.Int64()), nil
+}
+
+func resolvePlacement(intent PlacementIntent, candidates []PlacementCandidate, selectIndex func(int) (int, error)) (string, error) {
+ if !PlacementSupported(intent.Network, intent.Provider) {
+ return "", fmt.Errorf("placement %s/%s is unsupported", intent.Provider, intent.Network)
+ }
+ eligible := eligiblePlacementCandidates(intent, candidates)
+ if len(eligible) == 0 {
+ return "", fmt.Errorf("no eligible placement is available")
+ }
+ index, err := selectIndex(len(eligible))
+ if err != nil {
+ return "", err
+ }
+ if index < 0 || index >= len(eligible) {
+ return "", fmt.Errorf("placement selector returned invalid index %d", index)
+ }
+ return eligible[index].ID, nil
+}
+
+func ResolvePlacement(intent PlacementIntent, candidates []PlacementCandidate) (string, error) {
+ return resolvePlacement(intent, candidates, randomPlacementIndex)
+}
diff --git a/components/api-server/plugins/gateways/placement_test.go b/components/api-server/plugins/gateways/placement_test.go
new file mode 100644
index 000000000..8f1c3e2a8
--- /dev/null
+++ b/components/api-server/plugins/gateways/placement_test.go
@@ -0,0 +1,90 @@
+package gateways
+
+import (
+ "errors"
+ "testing"
+ "time"
+)
+
+func TestPlacementControlPlaneConnectedUsesHeartbeatWindow(t *testing.T) {
+ now := time.Now()
+ healthy := now.Add(-time.Minute)
+ offline := now.Add(-31 * time.Minute)
+ if !PlacementControlPlaneConnected(&healthy, now) {
+ t.Fatal("expected a recent heartbeat to be connected")
+ }
+ if PlacementControlPlaneConnected(&offline, now) || PlacementControlPlaneConnected(nil, now) {
+ t.Fatal("expected an absent or stale heartbeat to be disconnected")
+ }
+}
+
+func TestPlacementSupported(t *testing.T) {
+ tests := []struct {
+ network string
+ provider string
+ want bool
+ }{
+ {network: "public", provider: "aws", want: true},
+ {network: "vpn", provider: "aws", want: true},
+ {network: "public", provider: "ibm", want: true},
+ {network: "vpn", provider: "ibm", want: false},
+ }
+ for _, test := range tests {
+ if got := PlacementSupported(test.network, test.provider); got != test.want {
+ t.Errorf("PlacementSupported(%q, %q) = %t, want %t", test.network, test.provider, got, test.want)
+ }
+ }
+}
+
+func TestResolvePlacementSelectsRandomEligibleCandidate(t *testing.T) {
+ selectedSize := 0
+ got, err := resolvePlacement(PlacementIntent{Network: "public", Provider: "aws"}, []PlacementCandidate{
+ {ID: "disconnected", Provider: "aws", Connected: false},
+ {ID: "ibm", Provider: "ibm", Connected: true},
+ {ID: "aws-a", Provider: "aws", Visibility: "public", Connected: true},
+ {ID: "aws-b", Provider: "aws", Visibility: "public", Connected: true},
+ }, func(size int) (int, error) {
+ selectedSize = size
+ return 1, nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if selectedSize != 2 || got != "aws-b" {
+ t.Fatalf("eligible size = %d, selected = %q; want 2 and aws-b", selectedSize, got)
+ }
+}
+
+func TestResolvePlacementRejectsUnsupportedAndUnavailablePlacement(t *testing.T) {
+ if _, err := ResolvePlacement(PlacementIntent{Network: "vpn", Provider: "ibm"}, nil); err == nil {
+ t.Fatal("expected unsupported placement error")
+ }
+ if _, err := ResolvePlacement(PlacementIntent{Network: "public", Provider: "aws"}, nil); err == nil {
+ t.Fatal("expected unavailable placement error")
+ }
+}
+
+func TestResolvePlacementReturnsRandomSourceFailure(t *testing.T) {
+ want := errors.New("random unavailable")
+ _, err := resolvePlacement(
+ PlacementIntent{Network: "public", Provider: "aws"},
+ []PlacementCandidate{{ID: "aws", Provider: "aws", Visibility: "public", Connected: true}},
+ func(int) (int, error) { return 0, want },
+ )
+ if !errors.Is(err, want) {
+ t.Fatalf("error = %v, want %v", err, want)
+ }
+}
+
+func TestManagedPlacementAvailableRequiresMatchingVisibility(t *testing.T) {
+ snapshot := placementSnapshot{providers: map[string]*providerPlacementState{
+ "aws": {candidates: []PlacementCandidate{{ID: "public-aws", Provider: "aws", Visibility: "public", Connected: true}}},
+ }}
+
+ if !managedPlacementAvailable(snapshot, "public", "aws") {
+ t.Fatal("expected public AWS placement to be available")
+ }
+ if managedPlacementAvailable(snapshot, "vpn", "aws") {
+ t.Fatal("expected VPN AWS placement to be unavailable without a VPN cluster")
+ }
+}
diff --git a/components/api-server/plugins/gateways/plugin.go b/components/api-server/plugins/gateways/plugin.go
index 0128e3c2a..d29fb92b1 100644
--- a/components/api-server/plugins/gateways/plugin.go
+++ b/components/api-server/plugins/gateways/plugin.go
@@ -2,12 +2,17 @@ package gateways
import (
"context"
+ "fmt"
"net/http"
+ "os"
+ "strings"
+ "time"
"github.com/gorilla/mux"
"google.golang.org/grpc"
pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1"
+ "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
"github.com/openshift-online/hypershell/components/api-server/pkg/rbac"
"github.com/openshift-online/hypershell/components/api-server/plugins/managedClusters"
"github.com/openshift-online/hypershell/components/api-server/plugins/roleBindings"
@@ -110,6 +115,130 @@ func registeredClusterLookup(s *environments.Services) RegisteredClusterLookup {
}
}
+type providerPlacementState struct {
+ candidates []PlacementCandidate
+}
+
+type placementSnapshot struct {
+ providers map[string]*providerPlacementState
+ localID string
+}
+
+type registeredPlacementService struct {
+ services *environments.Services
+ now func() time.Time
+}
+
+func newRegisteredPlacementService(s *environments.Services) *registeredPlacementService {
+ return ®isteredPlacementService{services: s, now: time.Now}
+}
+
+func normalizePlacementProvider(provider string) string {
+ provider = strings.ToLower(strings.TrimSpace(provider))
+ if provider == "ibm cloud" {
+ return "ibm"
+ }
+ return provider
+}
+
+func (p *registeredPlacementService) snapshot(ctx context.Context) (placementSnapshot, *errors.ServiceError) {
+ result := placementSnapshot{providers: map[string]*providerPlacementState{"aws": {}, "ibm": {}}}
+ svc := managedClusters.Service(p.services)
+ if svc == nil {
+ return result, errors.GeneralError("managed cluster service is not available")
+ }
+ clusters, svcErr := svc.All(ctx)
+ if svcErr != nil {
+ return result, svcErr
+ }
+ for _, cluster := range clusters {
+ if cluster == nil || cluster.OIDCSubject == "" || !PlacementControlPlaneConnected(cluster.LastSeenAt, p.now()) {
+ continue
+ }
+ provider := normalizePlacementProvider(cluster.Provider)
+ isLocal := cluster.Name == "local-kind"
+ if isLocal {
+ if os.Getenv("HYPERSHELL_PLATFORM_KIND") == "true" {
+ result.localID = cluster.ID
+ }
+ continue
+ }
+ state := result.providers[provider]
+ if state == nil {
+ continue
+ }
+ state.candidates = append(state.candidates, PlacementCandidate{ID: cluster.ID, Provider: provider, Visibility: cluster.Visibility, Connected: true})
+ }
+ return result, nil
+}
+
+func (p *registeredPlacementService) selectManaged(snapshot placementSnapshot, network, provider string) (string, error) {
+ state := snapshot.providers[provider]
+ if state == nil {
+ return "", fmt.Errorf("unsupported provider %q", provider)
+ }
+ return ResolvePlacement(PlacementIntent{Network: network, Provider: provider}, state.candidates)
+}
+
+func managedPlacementAvailable(snapshot placementSnapshot, network, provider string) bool {
+ state := snapshot.providers[provider]
+ if !PlacementSupported(network, provider) || state == nil {
+ return false
+ }
+ for _, candidate := range state.candidates {
+ if candidate.Visibility == network {
+ return true
+ }
+ }
+ return false
+}
+
+func (p *registeredPlacementService) Resolve(ctx context.Context, intent openapi.GatewayPlacementIntent) (string, *errors.ServiceError) {
+ snapshot, svcErr := p.snapshot(ctx)
+ if svcErr != nil {
+ return "", svcErr
+ }
+ if intent.Mode != nil && *intent.Mode == "local-kind" {
+ if os.Getenv("HYPERSHELL_PLATFORM_KIND") != "true" {
+ return "", errors.Validation("local-kind placement is only available in the Kind development environment")
+ }
+ if snapshot.localID == "" {
+ return "", errors.Validation("local-kind placement is unavailable")
+ }
+ return snapshot.localID, nil
+ }
+ if intent.Network == nil || intent.Provider == nil || !PlacementSupported(*intent.Network, *intent.Provider) {
+ return "", errors.Validation("the requested network and provider placement is unsupported")
+ }
+ selected, err := p.selectManaged(snapshot, *intent.Network, *intent.Provider)
+ if err != nil {
+ return "", errors.Validation("no eligible managed cluster is available for the requested placement")
+ }
+ return selected, nil
+}
+
+func (p *registeredPlacementService) Availability(ctx context.Context) (openapi.GatewayPlacementAvailability, *errors.ServiceError) {
+ snapshot, svcErr := p.snapshot(ctx)
+ if svcErr != nil {
+ return openapi.GatewayPlacementAvailability{}, svcErr
+ }
+ availability := openapi.GatewayPlacementAvailability{}
+ availability.AwsPublic = managedPlacementAvailable(snapshot, "public", "aws")
+ availability.AwsVpn = managedPlacementAvailable(snapshot, "vpn", "aws")
+ availability.IbmPublic = managedPlacementAvailable(snapshot, "public", "ibm")
+ availability.IbmVpn = false
+ if !availability.AwsPublic && !availability.AwsVpn {
+ reason := "no-eligible-cluster"
+ availability.AwsReason = &reason
+ }
+ if !availability.IbmPublic {
+ reason := "no-eligible-cluster"
+ availability.IbmReason = &reason
+ }
+ availability.LocalKind = os.Getenv("HYPERSHELL_PLATFORM_KIND") == "true" && snapshot.localID != ""
+ return availability, nil
+}
+
func init() {
registry.RegisterService("Gateways", func(env interface{}) interface{} {
return NewServiceLocator(env.(*environments.Env))
@@ -132,10 +261,12 @@ func init() {
})
ownerLookup = rbService
}
- gatewayHandler := NewGatewayHandler(Service(envServices), listService(envServices), ownerBinding, visibilityFilter, ownerLookup, registeredClusterLookup(envServices))
+ placement := newRegisteredPlacementService(envServices)
+ gatewayHandler := NewGatewayHandler(Service(envServices), listService(envServices), ownerBinding, visibilityFilter, ownerLookup, registeredClusterLookup(envServices), placement.Resolve, placement.Availability)
gatewaysRouter := apiV1Router.PathPrefix("/gateways").Subrouter()
gatewaysRouter.HandleFunc("", gatewayHandler.List).Methods(http.MethodGet)
+ gatewaysRouter.HandleFunc("/placement-availability", gatewayHandler.GetPlacementAvailability).Methods(http.MethodGet)
gatewaysRouter.HandleFunc("/{id}", gatewayHandler.Get).Methods(http.MethodGet)
gatewaysRouter.HandleFunc("", gatewayHandler.Create).Methods(http.MethodPost)
gatewaysRouter.HandleFunc("/{id}", gatewayHandler.Patch).Methods(http.MethodPatch)
@@ -172,7 +303,8 @@ func init() {
}
return nil
}
- pb.RegisterGatewayServiceServer(grpcServer, NewGatewayGRPCHandler(gatewayService, genericService, brokerFunc, registeredClusterLookup(envServices)))
+ placement := newRegisteredPlacementService(envServices)
+ pb.RegisterGatewayServiceServer(grpcServer, NewGatewayGRPCHandler(gatewayService, genericService, brokerFunc, registeredClusterLookup(envServices), placement.Resolve))
})
presenters.RegisterPath(Gateway{}, "gateways")
diff --git a/components/api-server/plugins/gateways/presenter.go b/components/api-server/plugins/gateways/presenter.go
index a94da0182..ad1b039b0 100644
--- a/components/api-server/plugins/gateways/presenter.go
+++ b/components/api-server/plugins/gateways/presenter.go
@@ -11,7 +11,8 @@ import (
func ConvertGateway(gateway openapi.GatewayCreateRequest) *Gateway {
c := &Gateway{}
c.Name = gateway.Name
- c.ClusterId = gateway.ClusterId
+ // Placement resolves the concrete cluster in the create handler before the
+ // Gateway model is persisted.
c.ExternalDns = gateway.ExternalDns
c.TlsMode = gateway.TlsMode
c.ServiceType = gateway.ServiceType
diff --git a/components/api-server/plugins/managedClusters/dao.go b/components/api-server/plugins/managedClusters/dao.go
index 7f4fd4853..484710155 100644
--- a/components/api-server/plugins/managedClusters/dao.go
+++ b/components/api-server/plugins/managedClusters/dao.go
@@ -24,8 +24,8 @@ type ManagedClusterDao interface {
// (oidc_subject, name) spans soft-deleted rows, so there is at most one.
FindDeletedBySubjectAndName(ctx context.Context, subject, name string) (*ManagedCluster, error)
// Restore clears deleted_at on a soft-deleted record, keeping its id, and
- // stamps last_seen_at.
- Restore(ctx context.Context, id string, lastSeenAt time.Time) (*ManagedCluster, error)
+ // atomically refreshes its placement metadata from the new registration.
+ Restore(ctx context.Context, id, provider, visibility string, lastSeenAt time.Time) (*ManagedCluster, error)
InventorySnapshot(ctx context.Context, evaluationTime time.Time) (*ClusterInventorySnapshot, error)
}
@@ -122,10 +122,16 @@ func (d *sqlManagedClusterDao) FindDeletedBySubjectAndName(ctx context.Context,
return &managedCluster, nil
}
-func (d *sqlManagedClusterDao) Restore(ctx context.Context, id string, lastSeenAt time.Time) (*ManagedCluster, error) {
+func (d *sqlManagedClusterDao) Restore(ctx context.Context, id, provider, visibility string, lastSeenAt time.Time) (*ManagedCluster, error) {
g2 := (*d.sessionFactory).New(ctx)
if err := g2.Unscoped().Model(&ManagedCluster{}).Where("id = ?", id).
- Updates(map[string]interface{}{"deleted_at": nil, "last_seen_at": lastSeenAt, "updated_at": time.Now()}).Error; err != nil {
+ Updates(map[string]interface{}{
+ "deleted_at": nil,
+ "provider": provider,
+ "visibility": visibility,
+ "last_seen_at": lastSeenAt,
+ "updated_at": time.Now(),
+ }).Error; err != nil {
db.MarkForRollback(ctx, err)
return nil, err
}
diff --git a/components/api-server/plugins/managedClusters/handler.go b/components/api-server/plugins/managedClusters/handler.go
index d16d811cc..708eea1e2 100644
--- a/components/api-server/plugins/managedClusters/handler.go
+++ b/components/api-server/plugins/managedClusters/handler.go
@@ -59,6 +59,18 @@ func (h managedClusterHandler) Register(w http.ResponseWriter, r *http.Request)
))
return
}
+ if req.GetProvider() != "aws" && req.GetProvider() != "ibm" && req.GetProvider() != "kind" {
+ handlers.HandleError(r.Context(), w, errors.Validation("provider must be aws, ibm, or kind"))
+ return
+ }
+ if req.GetVisibility() != "public" && req.GetVisibility() != "vpn" {
+ handlers.HandleError(r.Context(), w, errors.Validation("visibility must be public or vpn"))
+ return
+ }
+ if req.GetProvider() == "ibm" && req.GetVisibility() == "vpn" {
+ handlers.HandleError(r.Context(), w, errors.Validation("IBM placement does not support VPN visibility"))
+ return
+ }
ctx := r.Context()
token, tokenErr := auth.TokenFromContext(ctx)
@@ -82,7 +94,7 @@ func (h managedClusterHandler) Register(w http.ResponseWriter, r *http.Request)
description = *req.Description
}
- cluster, created, svcErr := h.managedCluster.Register(ctx, req.Name, description, oidcSubject)
+ cluster, created, svcErr := h.managedCluster.Register(ctx, req.Name, description, req.GetProvider(), req.GetVisibility(), oidcSubject)
if svcErr != nil {
handlers.HandleError(r.Context(), w, svcErr)
return
diff --git a/components/api-server/plugins/managedClusters/migration.go b/components/api-server/plugins/managedClusters/migration.go
index 38e561aeb..3cc75cce2 100644
--- a/components/api-server/plugins/managedClusters/migration.go
+++ b/components/api-server/plugins/managedClusters/migration.go
@@ -40,6 +40,18 @@ func migrationAddRegistrationFields() *gormigrate.Migration {
}
}
+func migrationAddPlacementVisibility() *gormigrate.Migration {
+ return &gormigrate.Migration{
+ ID: "2026092900000001",
+ Migrate: func(tx *gorm.DB) error {
+ return tx.Exec(`ALTER TABLE managed_clusters ADD COLUMN IF NOT EXISTS visibility TEXT`).Error
+ },
+ Rollback: func(tx *gorm.DB) error {
+ return tx.Exec(`ALTER TABLE managed_clusters DROP COLUMN IF EXISTS visibility`).Error
+ },
+ }
+}
+
// migrationUniqueName makes managed_clusters.name unique across all live
// records, registered or not, so discovery by name (seed scripts, e2e,
// operators) is unambiguous and a registration can never create a second
diff --git a/components/api-server/plugins/managedClusters/mock_dao.go b/components/api-server/plugins/managedClusters/mock_dao.go
index 3eff7bf52..d502e1513 100644
--- a/components/api-server/plugins/managedClusters/mock_dao.go
+++ b/components/api-server/plugins/managedClusters/mock_dao.go
@@ -77,7 +77,7 @@ func (d *managedClusterDaoMock) FindDeletedBySubjectAndName(ctx context.Context,
return nil, gorm.ErrRecordNotFound
}
-func (d *managedClusterDaoMock) Restore(ctx context.Context, id string, lastSeenAt time.Time) (*ManagedCluster, error) {
+func (d *managedClusterDaoMock) Restore(ctx context.Context, id, provider, visibility string, lastSeenAt time.Time) (*ManagedCluster, error) {
return nil, errors.NotImplemented("ManagedCluster").AsError()
}
diff --git a/components/api-server/plugins/managedClusters/model.go b/components/api-server/plugins/managedClusters/model.go
index 149830caf..e21cdb554 100644
--- a/components/api-server/plugins/managedClusters/model.go
+++ b/components/api-server/plugins/managedClusters/model.go
@@ -13,6 +13,7 @@ type ManagedCluster struct {
hypershellapi.TraceMeta
Name string `json:"name"`
Provider string `json:"provider"`
+ Visibility string `json:"visibility" gorm:"column:visibility"`
Region *string `json:"region"`
KubeconfigSecret string `json:"kubeconfig_secret"`
Status *string `json:"status"`
diff --git a/components/api-server/plugins/managedClusters/plugin.go b/components/api-server/plugins/managedClusters/plugin.go
index aab2a17d8..4cf073565 100644
--- a/components/api-server/plugins/managedClusters/plugin.go
+++ b/components/api-server/plugins/managedClusters/plugin.go
@@ -100,5 +100,6 @@ func init() {
db.RegisterMigration(migrationDropFleetId())
db.RegisterMigration(migrationAddTraceContext())
db.RegisterMigration(migrationAddRegistrationFields())
+ db.RegisterMigration(migrationAddPlacementVisibility())
db.RegisterMigration(migrationUniqueName())
}
diff --git a/components/api-server/plugins/managedClusters/presenter.go b/components/api-server/plugins/managedClusters/presenter.go
index 7404edc94..69c31b364 100644
--- a/components/api-server/plugins/managedClusters/presenter.go
+++ b/components/api-server/plugins/managedClusters/presenter.go
@@ -15,6 +15,7 @@ func ConvertManagedCluster(managedCluster openapi.ManagedCluster) *ManagedCluste
}
c.Name = managedCluster.Name
c.Provider = managedCluster.Provider
+ c.Visibility = managedCluster.GetVisibility()
c.Region = managedCluster.Region
c.KubeconfigSecret = managedCluster.KubeconfigSecret
c.Status = managedCluster.Status
@@ -38,6 +39,12 @@ func PresentManagedCluster(managedCluster *ManagedCluster) openapi.ManagedCluste
UpdatedAt: openapi.PtrTime(managedCluster.UpdatedAt),
Name: managedCluster.Name,
Provider: managedCluster.Provider,
+ Visibility: func() *string {
+ if managedCluster.Visibility == "" {
+ return nil
+ }
+ return &managedCluster.Visibility
+ }(),
Region: managedCluster.Region,
KubeconfigSecret: managedCluster.KubeconfigSecret,
Status: managedCluster.Status,
diff --git a/components/api-server/plugins/managedClusters/registration_integration_test.go b/components/api-server/plugins/managedClusters/registration_integration_test.go
index 18b4fe47f..ac2b5e72e 100644
--- a/components/api-server/plugins/managedClusters/registration_integration_test.go
+++ b/components/api-server/plugins/managedClusters/registration_integration_test.go
@@ -23,7 +23,7 @@ func controlPlaneContext(h *test.Helper, subject string) context.Context {
func register(client *openapi.APIClient, ctx context.Context, name string) (*openapi.ManagedClusterRegistrationResponse, *http.Response, error) {
return client.DefaultAPI.RegisterManagedCluster(ctx).
- ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{Name: name}).
+ ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{Name: name, Provider: "aws", Visibility: "public"}).
Execute()
}
@@ -58,6 +58,38 @@ func TestManagedClusterRegistrationIdempotent(t *testing.T) {
Expect(resp.StatusCode).To(Equal(http.StatusConflict))
}
+// Re-registration restores placement metadata from the current registration,
+// rather than retaining the provider and visibility from the deleted record.
+func TestManagedClusterRegistrationRestoreRefreshesPlacementMetadata(t *testing.T) {
+ h, client := test.RegisterIntegration(t)
+ ctx := controlPlaneContext(h, uniqueName("cp"))
+ name := uniqueName("hyp0-mc")
+
+ first, resp, err := register(client, ctx, name)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusCreated))
+
+ resp, err = client.DefaultAPI.DeleteManagedCluster(ctx, first.ClusterId).Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusNoContent))
+
+ restored, resp, err := client.DefaultAPI.RegisterManagedCluster(ctx).
+ ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{
+ Name: name,
+ Provider: "ibm",
+ Visibility: "public",
+ }).Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusCreated))
+ Expect(restored.ClusterId).To(Equal(first.ClusterId))
+
+ cluster, _, err := client.DefaultAPI.GetManagedCluster(ctx, restored.ClusterId).Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cluster.Provider).To(Equal("ibm"))
+ Expect(cluster.GetVisibility()).To(Equal("public"))
+ Expect(cluster.LastSeenAt).NotTo(BeNil())
+}
+
// Name Collision Is a Conflict: a name held by a manually created record (empty
// oidc_subject) is a 409 naming the record, nothing is created or adopted, and
// deleting the record lets the control plane register (201, new id).
diff --git a/components/api-server/plugins/managedClusters/service.go b/components/api-server/plugins/managedClusters/service.go
index ba9138546..b8599f319 100644
--- a/components/api-server/plugins/managedClusters/service.go
+++ b/components/api-server/plugins/managedClusters/service.go
@@ -31,7 +31,7 @@ type ManagedClusterService interface {
// held by a record with a different or empty oidc_subject, or a subject
// already registered under another name, is a 409 Conflict: registration
// never creates a duplicate name and never adopts an existing record.
- Register(ctx context.Context, name, description, oidcSubject string) (*ManagedCluster, bool, *errors.ServiceError)
+ Register(ctx context.Context, name, description, provider, visibility, oidcSubject string) (*ManagedCluster, bool, *errors.ServiceError)
// FindRegisteredBySubject returns the ManagedCluster a control plane
// registered under the given OIDC subject, or (nil, nil) when the subject
// has not registered. An empty subject never matches.
@@ -169,7 +169,7 @@ func (s *sqlManagedClusterService) All(ctx context.Context) (ManagedClusterList,
return managedClusters, nil
}
-func (s *sqlManagedClusterService) Register(ctx context.Context, name, description, oidcSubject string) (*ManagedCluster, bool, *errors.ServiceError) {
+func (s *sqlManagedClusterService) Register(ctx context.Context, name, description, provider, visibility, oidcSubject string) (*ManagedCluster, bool, *errors.ServiceError) {
lockOwnerID, lockErr := s.lockFactory.NewAdvisoryLock(ctx, oidcSubject, managedClustersLockType)
if lockErr != nil {
return nil, false, errors.DatabaseAdvisoryLock(lockErr)
@@ -187,6 +187,8 @@ func (s *sqlManagedClusterService) Register(ctx context.Context, name, descripti
}
now := time.Now()
existing.LastSeenAt = &now
+ existing.Provider = provider
+ existing.Visibility = visibility
updated, replaceErr := s.managedClusterDao.Replace(ctx, existing)
if replaceErr != nil {
return nil, false, services.HandleUpdateError("ManagedCluster", replaceErr)
@@ -219,7 +221,7 @@ func (s *sqlManagedClusterService) Register(ctx context.Context, name, descripti
return nil, false, errors.GeneralError("registration lookup failed: %s", delErr)
}
if deleted != nil {
- restored, restoreErr := s.managedClusterDao.Restore(ctx, deleted.ID, now)
+ restored, restoreErr := s.managedClusterDao.Restore(ctx, deleted.ID, provider, visibility, now)
if restoreErr != nil {
if isUniqueViolation(restoreErr) {
if winner, err := s.managedClusterDao.FindByName(ctx, name); err == nil && winner != nil {
@@ -242,6 +244,8 @@ func (s *sqlManagedClusterService) Register(ctx context.Context, name, descripti
Name: name,
OIDCSubject: oidcSubject,
LastSeenAt: &now,
+ Provider: provider,
+ Visibility: visibility,
}
cluster.CaptureTraceContext(ctx)
created, createErr := s.managedClusterDao.Create(ctx, cluster)
diff --git a/components/api-server/plugins/rbac/cluster_binding_integration_test.go b/components/api-server/plugins/rbac/cluster_binding_integration_test.go
index cdef5e5b5..03e468d83 100644
--- a/components/api-server/plugins/rbac/cluster_binding_integration_test.go
+++ b/components/api-server/plugins/rbac/cluster_binding_integration_test.go
@@ -14,7 +14,7 @@ import (
"google.golang.org/grpc/status"
pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1"
- "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
+ "github.com/openshift-online/hypershell/components/api-server/plugins/gateways"
"github.com/openshift-online/hypershell/components/api-server/plugins/managedClusters"
"github.com/openshift-online/hypershell/components/api-server/test"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/api"
@@ -50,7 +50,7 @@ func registerTestClusterWithSubject(t *testing.T) (string, string) {
suffix := strings.ToLower(api.NewID())
subject := "cp-" + suffix
svc := managedClusters.Service(&environments.Environment().Services)
- cluster, _, svcErr := svc.Register(context.Background(), fmt.Sprintf("mc-%s", suffix), "", subject)
+ cluster, _, svcErr := svc.Register(context.Background(), fmt.Sprintf("mc-%s", suffix), "", "aws", "public", subject)
if svcErr != nil {
t.Fatalf("register test managed cluster: %v", svcErr)
}
@@ -68,7 +68,7 @@ func registerTestCluster(t *testing.T) string {
// own cluster. Own id is accepted and delivers only that cluster's gateways; a
// foreign id is PERMISSION_DENIED; a missing id is INVALID_ARGUMENT.
func TestGRPCClusterCallerBinding(t *testing.T) {
- h, client := test.RegisterIntegration(t)
+ h, _ := test.RegisterIntegration(t)
h.StartControllersServer()
ownID, subject := registerTestClusterWithSubject(t)
@@ -114,23 +114,20 @@ func TestGRPCClusterCallerBinding(t *testing.T) {
_, err = stream.Header()
Expect(err).NotTo(HaveOccurred(), "own cluster watch must be accepted")
- foreignGw, _, err := client.DefaultAPI.CreateGateway(userCtx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: "binding-foreign", ClusterId: foreignID,
- }).Execute()
- Expect(err).NotTo(HaveOccurred())
- ownGw, _, err := client.DefaultAPI.CreateGateway(userCtx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: "binding-own", ClusterId: ownID,
- }).Execute()
- Expect(err).NotTo(HaveOccurred())
+ service := gateways.Service(&environments.Environment().Services)
+ foreignGw, svcErr := service.Create(userCtx, &gateways.Gateway{Name: "binding-foreign", ClusterId: foreignID})
+ Expect(svcErr).NotTo(HaveOccurred())
+ ownGw, svcErr := service.Create(userCtx, &gateways.Gateway{Name: "binding-own", ClusterId: ownID})
+ Expect(svcErr).NotTo(HaveOccurred())
for {
evt, recvErr := stream.Recv()
Expect(recvErr).NotTo(HaveOccurred(), "stream closed before the own-cluster event")
- Expect(evt.ResourceId).NotTo(Equal(*foreignGw.Id), "a foreign cluster's gateway was delivered")
+ Expect(evt.ResourceId).NotTo(Equal(foreignGw.ID), "a foreign cluster's gateway was delivered")
if evt.GetGateway() != nil {
Expect(evt.GetGateway().GetClusterId()).To(Equal(ownID))
}
- if evt.ResourceId == *ownGw.Id {
+ if evt.ResourceId == ownGw.ID {
break
}
}
diff --git a/components/api-server/plugins/roleBindings/grpc_integration_test.go b/components/api-server/plugins/roleBindings/grpc_integration_test.go
index 2f6c12d12..ab086e90b 100644
--- a/components/api-server/plugins/roleBindings/grpc_integration_test.go
+++ b/components/api-server/plugins/roleBindings/grpc_integration_test.go
@@ -105,22 +105,20 @@ func registerRoleBindingTestCluster(t *testing.T) string {
t.Helper()
suffix := strings.ToLower(api.NewID())
svc := managedClusters.Service(&environments.Environment().Services)
- cluster, _, svcErr := svc.Register(context.Background(), fmt.Sprintf("mc-%s", suffix), "", "cp-"+suffix)
+ cluster, _, svcErr := svc.Register(context.Background(), fmt.Sprintf("mc-%s", suffix), "", "aws", "public", "cp-"+suffix)
if svcErr != nil {
t.Fatalf("register test managed cluster: %v", svcErr)
}
return cluster.ID
}
-// createGatewayWithOwnerBinding creates a gateway on clusterID through the REST
-// API, grants userID gateway:owner on it, and returns the gateway id and that
-// binding's id.
+// createGatewayWithOwnerBinding creates an internal gateway fixture on
+// clusterID, grants userID gateway:owner on it, and returns both ids.
func createGatewayWithOwnerBinding(ctx context.Context, client *openapi.APIClient, userID, name, clusterID string) (string, string) {
- gw, _, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: name, ClusterId: clusterID,
- }).Execute()
- Expect(err).NotTo(HaveOccurred(), "create gateway %s", name)
- gatewayID := *gw.Id
+ _ = client
+ gw, svcErr := gateways.Service(&environments.Environment().Services).Create(ctx, &gateways.Gateway{Name: name, ClusterId: clusterID})
+ Expect(svcErr).NotTo(HaveOccurred(), "create gateway %s", name)
+ gatewayID := gw.ID
// The integration environment does not provision the caller into the
// request context, so the create path does not bootstrap the owner binding;
diff --git a/components/api-server/proto/hypershell/v1/gateways.proto b/components/api-server/proto/hypershell/v1/gateways.proto
index e25985818..7ce3043e1 100644
--- a/components/api-server/proto/hypershell/v1/gateways.proto
+++ b/components/api-server/proto/hypershell/v1/gateways.proto
@@ -20,6 +20,20 @@ message ProvisioningCondition {
string message = 3;
}
+message GatewayPlacementIntent {
+ oneof placement {
+ NetworkProviderPlacement network_provider = 1;
+ LocalKindPlacement local_kind = 2;
+ }
+}
+
+message NetworkProviderPlacement {
+ string network = 1;
+ string provider = 2;
+}
+
+message LocalKindPlacement {}
+
message Gateway {
ObjectReference metadata = 1;
string name = 2;
@@ -74,6 +88,7 @@ message CreateGatewayRequest {
optional string oidc = 14;
optional string route = 15;
optional string credential_driver = 17;
+ GatewayPlacementIntent placement = 18;
}
message CreateGatewayResponse {
diff --git a/components/api-server/test/authz/authz_integration_test.go b/components/api-server/test/authz/authz_integration_test.go
index 979f82d0f..ac74e9784 100644
--- a/components/api-server/test/authz/authz_integration_test.go
+++ b/components/api-server/test/authz/authz_integration_test.go
@@ -258,11 +258,14 @@ func TestReRegistrationAfterDeleteDoesNotReclaimATakenName(t *testing.T) {
func createGateway(t *testing.T, client *openapi.APIClient, owner principal, clusterID string) *openapi.Gateway {
t.Helper()
- gw, httpResp, err := client.DefaultAPI.CreateGateway(owner.rest).GatewayCreateRequest(openapi.GatewayCreateRequest{
- Name: fmt.Sprintf("gw-%s", strings.ToLower(api.NewID())[:10]), ClusterId: clusterID,
- }).Execute()
- Expect(err).NotTo(HaveOccurred(), "create gateway: HTTP %d %s", statusOf(httpResp), test.APIErrorReason(err))
- return gw
+ _ = client
+ created, err := pb.NewGatewayServiceClient(owner.conn).CreateGateway(context.Background(), &pb.CreateGatewayRequest{
+ Name: fmt.Sprintf("gw-%s", strings.ToLower(api.NewID())[:10]),
+ ClusterId: clusterID,
+ })
+ Expect(err).NotTo(HaveOccurred(), "create gateway")
+ id := created.GetGateway().GetMetadata().GetId()
+ return &openapi.Gateway{Id: &id, Namespace: created.GetGateway().GetNamespace()}
}
func TestGRPCControlPlaneWritesAreScopedToItsCluster(t *testing.T) {
diff --git a/components/control-plane/cmd/hypershell-controller/main.go b/components/control-plane/cmd/hypershell-controller/main.go
index 7fa19ba96..eb0dd16a9 100644
--- a/components/control-plane/cmd/hypershell-controller/main.go
+++ b/components/control-plane/cmd/hypershell-controller/main.go
@@ -291,7 +291,7 @@ func main() {
// service-account provisioner above is not cluster-scoped and is already
// serving, so the pod's probes stay green while registration retries (for
// example while the hub API server is still starting).
- regClient := registration.NewClient(cfg.APIServerURL, cfg.ManagedClusterName, tokenProvider)
+ regClient := registration.NewClientWithPlacement(cfg.APIServerURL, cfg.ManagedClusterName, cfg.ManagedClusterProvider, cfg.ManagedClusterVisibility, tokenProvider)
clusterID, regErr := registerWithBackoff(ctx, regClient, time.Second)
if regErr != nil {
if ctx.Err() != nil {
diff --git a/components/control-plane/internal/config/config.go b/components/control-plane/internal/config/config.go
index b0a772f22..0a618b6a2 100644
--- a/components/control-plane/internal/config/config.go
+++ b/components/control-plane/internal/config/config.go
@@ -39,7 +39,9 @@ type Config struct {
// unregistered (unfiltered) mode, and a cluster id is never read from
// configuration. Sourced from HYPERSHELL_MANAGED_CLUSTER_NAME.
// See specs/platform/control-plane.spec.md ("Mandatory Cluster Identity").
- ManagedClusterName string
+ ManagedClusterName string
+ ManagedClusterProvider string
+ ManagedClusterVisibility string
// OIDC client credentials used both for registration and as per-RPC bearer
// credentials on every gRPC call (including the watch streams). All three
@@ -102,6 +104,8 @@ func Load() (*Config, error) {
Namespace: getEnv("HYPERSHELL_NAMESPACE", "hypershell"),
LogLevel: strings.ToLower(getEnv("HYPERSHELL_LOG_LEVEL", "info")),
ManagedClusterName: getEnv("HYPERSHELL_MANAGED_CLUSTER_NAME", ""),
+ ManagedClusterProvider: strings.ToLower(strings.TrimSpace(getEnv("HYPERSHELL_MANAGED_CLUSTER_PROVIDER", ""))),
+ ManagedClusterVisibility: strings.ToLower(strings.TrimSpace(getEnv("HYPERSHELL_MANAGED_CLUSTER_VISIBILITY", ""))),
OIDCIssuer: getEnv("OIDC_ISSUER", ""),
OIDCClientID: getEnv("OIDC_CLIENT_ID", ""),
OIDCClientSecret: getEnv("OIDC_CLIENT_SECRET", ""),
@@ -134,10 +138,18 @@ func Load() (*Config, error) {
// the first missing variable.
required := []struct{ name, value string }{
{"HYPERSHELL_MANAGED_CLUSTER_NAME", cfg.ManagedClusterName},
+ {"HYPERSHELL_MANAGED_CLUSTER_PROVIDER", cfg.ManagedClusterProvider},
+ {"HYPERSHELL_MANAGED_CLUSTER_VISIBILITY", cfg.ManagedClusterVisibility},
{"OIDC_ISSUER", cfg.OIDCIssuer},
{"OIDC_CLIENT_ID", cfg.OIDCClientID},
{"OIDC_CLIENT_SECRET", cfg.OIDCClientSecret},
}
+ if (cfg.ManagedClusterProvider != "aws" && cfg.ManagedClusterProvider != "ibm" && cfg.ManagedClusterProvider != "kind") ||
+ (cfg.ManagedClusterVisibility != "public" && cfg.ManagedClusterVisibility != "vpn") ||
+ (cfg.ManagedClusterProvider == "ibm" && cfg.ManagedClusterVisibility == "vpn") ||
+ (cfg.ManagedClusterProvider == "kind" && cfg.ManagedClusterVisibility != "public") {
+ return nil, fmt.Errorf("invalid managed cluster placement: provider=%q visibility=%q", cfg.ManagedClusterProvider, cfg.ManagedClusterVisibility)
+ }
for _, r := range required {
if strings.TrimSpace(r.value) == "" {
return nil, fmt.Errorf("%s is required: every control plane registers with the hub as a managed cluster (specs/platform/control-plane.spec.md, Mandatory Cluster Identity)", r.name)
diff --git a/components/control-plane/internal/config/config_test.go b/components/control-plane/internal/config/config_test.go
index 50a11dd51..e1016fcde 100644
--- a/components/control-plane/internal/config/config_test.go
+++ b/components/control-plane/internal/config/config_test.go
@@ -12,6 +12,8 @@ func setRequiredEnv(t *testing.T) {
t.Helper()
t.Setenv("HYPERSHELL_GRPC_SERVER_ADDR", "localhost:9000")
t.Setenv("HYPERSHELL_MANAGED_CLUSTER_NAME", "local-kind")
+ t.Setenv("HYPERSHELL_MANAGED_CLUSTER_PROVIDER", "kind")
+ t.Setenv("HYPERSHELL_MANAGED_CLUSTER_VISIBILITY", "public")
t.Setenv("OIDC_ISSUER", "http://keycloak.example/realms/hypershell")
t.Setenv("OIDC_CLIENT_ID", "hypershell-control-plane")
t.Setenv("OIDC_CLIENT_SECRET", "secret")
diff --git a/components/control-plane/internal/helm/values.go b/components/control-plane/internal/helm/values.go
index a05edc980..60ac82127 100644
--- a/components/control-plane/internal/helm/values.go
+++ b/components/control-plane/internal/helm/values.go
@@ -146,6 +146,11 @@ func (b *ValuesBuilder) buildCoreValues(values map[string]interface{}) error {
// Workload configuration
setNestedValue(values, "deployment", "workload", "kind")
setNestedValue(values, 1, "replicaCount")
+ // Keep the gateway workload requests and limits explicit.
+ setNestedValue(values, map[string]interface{}{
+ "requests": map[string]interface{}{"cpu": "100m", "memory": "256Mi"},
+ "limits": map[string]interface{}{"cpu": "500m", "memory": "512Mi"},
+ }, "resources")
// Gateway container resources. Always set: the upstream chart defaults to
// `resources: {}`, which would run the gateway BestEffort.
diff --git a/components/control-plane/internal/registration/client.go b/components/control-plane/internal/registration/client.go
index ab9b8f451..b9eb7aefa 100644
--- a/components/control-plane/internal/registration/client.go
+++ b/components/control-plane/internal/registration/client.go
@@ -34,22 +34,32 @@ type TokenSource interface {
type Client struct {
apiServerURL string
clusterName string
+ provider string
+ visibility string
tokens TokenSource
httpClient *http.Client
}
// NewClient creates a registration Client.
func NewClient(apiServerURL, clusterName string, tokens TokenSource) *Client {
+ return NewClientWithPlacement(apiServerURL, clusterName, "", "", tokens)
+}
+
+func NewClientWithPlacement(apiServerURL, clusterName, provider, visibility string, tokens TokenSource) *Client {
return &Client{
apiServerURL: strings.TrimRight(apiServerURL, "/"),
clusterName: clusterName,
+ provider: provider,
+ visibility: visibility,
tokens: tokens,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
type registrationRequest struct {
- Name string `json:"name"`
+ Name string `json:"name"`
+ Provider string `json:"provider,omitempty"`
+ Visibility string `json:"visibility,omitempty"`
}
type registrationResponse struct {
@@ -66,7 +76,7 @@ func (c *Client) Register(ctx context.Context) (string, error) {
return "", fmt.Errorf("get OIDC token: %w", err)
}
- body, err := json.Marshal(registrationRequest{Name: c.clusterName})
+ body, err := json.Marshal(registrationRequest{Name: c.clusterName, Provider: c.provider, Visibility: c.visibility})
if err != nil {
return "", fmt.Errorf("marshal registration request: %w", err)
}
diff --git a/components/sdk-go/client/agent_runtime_api.go b/components/sdk-go/client/agent_runtime_api.go
index 9c34a007e..ac0a78abc 100644
--- a/components/sdk-go/client/agent_runtime_api.go
+++ b/components/sdk-go/client/agent_runtime_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/client.go b/components/sdk-go/client/client.go
index a70246831..fd3d71acd 100644
--- a/components/sdk-go/client/client.go
+++ b/components/sdk-go/client/client.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/gateway_api.go b/components/sdk-go/client/gateway_api.go
index 9b4e06763..1fe9144bf 100644
--- a/components/sdk-go/client/gateway_api.go
+++ b/components/sdk-go/client/gateway_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
@@ -22,6 +22,14 @@ func (c *Client) Gateways() *GatewayAPI {
return &GatewayAPI{client: c}
}
+func (a *GatewayAPI) PlacementAvailability(ctx context.Context) (*types.GatewayPlacementAvailability, error) {
+ var result types.GatewayPlacementAvailability
+ if err := a.client.do(ctx, http.MethodGet, "/gateways/placement-availability", nil, http.StatusOK, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
func (a *GatewayAPI) Create(ctx context.Context, resource *types.Gateway) (*types.Gateway, error) {
body, err := json.Marshal(resource)
if err != nil {
diff --git a/components/sdk-go/client/inference_route_api.go b/components/sdk-go/client/inference_route_api.go
index 6b5487cae..b94b5a7f8 100644
--- a/components/sdk-go/client/inference_route_api.go
+++ b/components/sdk-go/client/inference_route_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/iterator.go b/components/sdk-go/client/iterator.go
index 6ec370aab..fed188840 100644
--- a/components/sdk-go/client/iterator.go
+++ b/components/sdk-go/client/iterator.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/managed_cluster_api.go b/components/sdk-go/client/managed_cluster_api.go
index 9ac26b306..43fb489fd 100644
--- a/components/sdk-go/client/managed_cluster_api.go
+++ b/components/sdk-go/client/managed_cluster_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/open_shell_gateway_service_account_api.go b/components/sdk-go/client/open_shell_gateway_service_account_api.go
index b059bfc4a..8908955a5 100644
--- a/components/sdk-go/client/open_shell_gateway_service_account_api.go
+++ b/components/sdk-go/client/open_shell_gateway_service_account_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
@@ -28,7 +28,7 @@ func (a *OpenShellGatewayServiceAccountAPI) Create(ctx context.Context, gatewayI
return nil, fmt.Errorf("marshal open_shell_gateway_service_account create request: %w", err)
}
var result types.OpenShellGatewayServiceAccountCreateResponse
- if _, err := a.client.doAny(ctx, http.MethodPost, fmt.Sprintf("/api/hypershell/v1/gateways/%s/service_accounts", url.PathEscape(gatewayID)), body, &result, http.StatusCreated); err != nil {
+ if _, err := a.client.doAny(ctx, http.MethodPost, fmt.Sprintf("/v1/gateways/%s/service_accounts", url.PathEscape(gatewayID)), body, &result, http.StatusCreated); err != nil {
return nil, err
}
return &result, nil
@@ -36,14 +36,14 @@ func (a *OpenShellGatewayServiceAccountAPI) Create(ctx context.Context, gatewayI
func (a *OpenShellGatewayServiceAccountAPI) Get(ctx context.Context, gatewayID string, serviceAccountID string) (*types.OpenShellGatewayServiceAccountGetResponse, error) {
var result types.OpenShellGatewayServiceAccountGetResponse
- if _, err := a.client.doAny(ctx, http.MethodGet, fmt.Sprintf("/api/hypershell/v1/gateways/%s/service_accounts/%s", url.PathEscape(gatewayID), url.PathEscape(serviceAccountID)), nil, &result, http.StatusOK); err != nil {
+ if _, err := a.client.doAny(ctx, http.MethodGet, fmt.Sprintf("/v1/gateways/%s/service_accounts/%s", url.PathEscape(gatewayID), url.PathEscape(serviceAccountID)), nil, &result, http.StatusOK); err != nil {
return nil, err
}
return &result, nil
}
func (a *OpenShellGatewayServiceAccountAPI) List(ctx context.Context, gatewayID string, opts *types.OpenShellGatewayServiceAccountListOptions) (*types.OpenShellGatewayServiceAccountList, error) {
- path := fmt.Sprintf("/api/hypershell/v1/gateways/%s/service_accounts", url.PathEscape(gatewayID))
+ path := fmt.Sprintf("/v1/gateways/%s/service_accounts", url.PathEscape(gatewayID))
if opts != nil {
params := url.Values{}
if opts.Page > 0 {
@@ -77,7 +77,7 @@ func (a *OpenShellGatewayServiceAccountAPI) List(ctx context.Context, gatewayID
func (a *OpenShellGatewayServiceAccountAPI) Revoke(ctx context.Context, gatewayID string, serviceAccountID string) (*types.OpenShellGatewayServiceAccountListItem, error) {
var result types.OpenShellGatewayServiceAccountListItem
- if _, err := a.client.doAny(ctx, http.MethodPost, fmt.Sprintf("/api/hypershell/v1/gateways/%s/service_accounts/%s", url.PathEscape(gatewayID), url.PathEscape(serviceAccountID))+"/revoke", nil, &result, http.StatusOK, http.StatusAccepted); err != nil {
+ if _, err := a.client.doAny(ctx, http.MethodPost, fmt.Sprintf("/v1/gateways/%s/service_accounts/%s", url.PathEscape(gatewayID), url.PathEscape(serviceAccountID))+"/revoke", nil, &result, http.StatusOK, http.StatusAccepted); err != nil {
return nil, err
}
return &result, nil
@@ -85,7 +85,7 @@ func (a *OpenShellGatewayServiceAccountAPI) Revoke(ctx context.Context, gatewayI
func (a *OpenShellGatewayServiceAccountAPI) Delete(ctx context.Context, gatewayID string, serviceAccountID string) (*types.OpenShellGatewayServiceAccountListItem, error) {
var result types.OpenShellGatewayServiceAccountListItem
- status, err := a.client.doAny(ctx, http.MethodDelete, fmt.Sprintf("/api/hypershell/v1/gateways/%s/service_accounts/%s", url.PathEscape(gatewayID), url.PathEscape(serviceAccountID)), nil, &result, http.StatusNoContent, http.StatusAccepted)
+ status, err := a.client.doAny(ctx, http.MethodDelete, fmt.Sprintf("/v1/gateways/%s/service_accounts/%s", url.PathEscape(gatewayID), url.PathEscape(serviceAccountID)), nil, &result, http.StatusNoContent, http.StatusAccepted)
if err != nil {
return nil, err
}
diff --git a/components/sdk-go/client/provider_binding_api.go b/components/sdk-go/client/provider_binding_api.go
index 795ab3d91..c539fed5a 100644
--- a/components/sdk-go/client/provider_binding_api.go
+++ b/components/sdk-go/client/provider_binding_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/provider_spec_api.go b/components/sdk-go/client/provider_spec_api.go
index 41f4a2384..f8c3df513 100644
--- a/components/sdk-go/client/provider_spec_api.go
+++ b/components/sdk-go/client/provider_spec_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/role_api.go b/components/sdk-go/client/role_api.go
index 9eab829a8..adfb88802 100644
--- a/components/sdk-go/client/role_api.go
+++ b/components/sdk-go/client/role_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/role_binding_api.go b/components/sdk-go/client/role_binding_api.go
index 626e20b43..c9259a293 100644
--- a/components/sdk-go/client/role_binding_api.go
+++ b/components/sdk-go/client/role_binding_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/sandbox_template_api.go b/components/sdk-go/client/sandbox_template_api.go
index 93f7df7ac..310df29ba 100644
--- a/components/sdk-go/client/sandbox_template_api.go
+++ b/components/sdk-go/client/sandbox_template_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/secret_source_api.go b/components/sdk-go/client/secret_source_api.go
index c30ec4853..ea473ef3c 100644
--- a/components/sdk-go/client/secret_source_api.go
+++ b/components/sdk-go/client/secret_source_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/client/user_api.go b/components/sdk-go/client/user_api.go
index 9cad600d0..92f87b2a6 100644
--- a/components/sdk-go/client/user_api.go
+++ b/components/sdk-go/client/user_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package client
diff --git a/components/sdk-go/types/agent_runtime.go b/components/sdk-go/types/agent_runtime.go
index cb7904353..b880acbe2 100644
--- a/components/sdk-go/types/agent_runtime.go
+++ b/components/sdk-go/types/agent_runtime.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/base.go b/components/sdk-go/types/base.go
index c150f3724..d48a83fc7 100644
--- a/components/sdk-go/types/base.go
+++ b/components/sdk-go/types/base.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/gateway.go b/components/sdk-go/types/gateway.go
index cbdfa67b4..a8e16cb90 100644
--- a/components/sdk-go/types/gateway.go
+++ b/components/sdk-go/types/gateway.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
@@ -9,6 +9,22 @@ import (
"fmt"
)
+type GatewayPlacementAvailability struct {
+ AwsPublic bool `json:"aws_public"`
+ AwsReason string `json:"aws_reason,omitempty"`
+ AwsVpn bool `json:"aws_vpn"`
+ IbmPublic bool `json:"ibm_public"`
+ IbmReason string `json:"ibm_reason,omitempty"`
+ IbmVpn bool `json:"ibm_vpn"`
+ LocalKind bool `json:"local_kind"`
+}
+
+type GatewayPlacementIntent struct {
+ Mode string `json:"mode,omitempty"`
+ Network string `json:"network,omitempty"`
+ Provider string `json:"provider,omitempty"`
+}
+
type Gateway struct {
ObjectReference
@@ -34,6 +50,8 @@ type Gateway struct {
Status string `json:"status,omitempty"`
SupervisorImage string `json:"supervisor_image,omitempty"`
TLSMode string `json:"tls_mode,omitempty"`
+
+ Placement GatewayPlacementIntent `json:"placement,omitempty"`
}
type GatewayList struct {
@@ -47,19 +65,15 @@ func (l *GatewayList) GetPage() int { return l.Page }
func (l *GatewayList) GetSize() int { return l.Size }
type GatewayBuilder struct {
- resource Gateway
- errors []error
+ resource Gateway
+ errors []error
+ placementSet bool
}
func NewGatewayBuilder() *GatewayBuilder {
return &GatewayBuilder{}
}
-func (b *GatewayBuilder) ClusterID(v string) *GatewayBuilder {
- b.resource.ClusterID = v
- return b
-}
-
func (b *GatewayBuilder) CredentialDriver(v string) *GatewayBuilder {
b.resource.CredentialDriver = v
return b
@@ -80,11 +94,22 @@ func (b *GatewayBuilder) Name(v string) *GatewayBuilder {
return b
}
+func (b *GatewayBuilder) Oidc(v string) *GatewayBuilder {
+ b.resource.Oidc = v
+ return b
+}
+
func (b *GatewayBuilder) Phase(v string) *GatewayBuilder {
b.resource.Phase = v
return b
}
+func (b *GatewayBuilder) Placement(v GatewayPlacementIntent) *GatewayBuilder {
+ b.resource.Placement = v
+ b.placementSet = true
+ return b
+}
+
func (b *GatewayBuilder) Route(v string) *GatewayBuilder {
b.resource.Route = v
return b
@@ -116,14 +141,11 @@ func (b *GatewayBuilder) TLSMode(v string) *GatewayBuilder {
}
func (b *GatewayBuilder) Build() (*Gateway, error) {
- if b.resource.ClusterID == "" {
- b.errors = append(b.errors, fmt.Errorf("cluster_id is required"))
- }
if b.resource.Name == "" {
b.errors = append(b.errors, fmt.Errorf("name is required"))
}
- if b.resource.Namespace == "" {
- b.errors = append(b.errors, fmt.Errorf("namespace is required"))
+ if !b.placementSet {
+ b.errors = append(b.errors, fmt.Errorf("placement is required"))
}
if len(b.errors) > 0 {
return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...))
diff --git a/components/sdk-go/types/inference_route.go b/components/sdk-go/types/inference_route.go
index 34b306770..67c73fa48 100644
--- a/components/sdk-go/types/inference_route.go
+++ b/components/sdk-go/types/inference_route.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/list_options.go b/components/sdk-go/types/list_options.go
index 42887b639..5a46974ad 100644
--- a/components/sdk-go/types/list_options.go
+++ b/components/sdk-go/types/list_options.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/managed_cluster.go b/components/sdk-go/types/managed_cluster.go
index e7b44512c..ca29d68b5 100644
--- a/components/sdk-go/types/managed_cluster.go
+++ b/components/sdk-go/types/managed_cluster.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
@@ -21,6 +21,7 @@ type ManagedCluster struct {
Provider string `json:"provider"`
Region string `json:"region,omitempty"`
Status string `json:"status,omitempty"`
+ Visibility string `json:"visibility,omitempty"`
}
type ManagedClusterList struct {
diff --git a/components/sdk-go/types/open_shell_gateway_service_account.go b/components/sdk-go/types/open_shell_gateway_service_account.go
index 5a4a9b2e3..e1ff22a04 100644
--- a/components/sdk-go/types/open_shell_gateway_service_account.go
+++ b/components/sdk-go/types/open_shell_gateway_service_account.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/provider_binding.go b/components/sdk-go/types/provider_binding.go
index fbc991fc2..d504db99e 100644
--- a/components/sdk-go/types/provider_binding.go
+++ b/components/sdk-go/types/provider_binding.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/provider_spec.go b/components/sdk-go/types/provider_spec.go
index 70c2638cb..358aeda3d 100644
--- a/components/sdk-go/types/provider_spec.go
+++ b/components/sdk-go/types/provider_spec.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/role.go b/components/sdk-go/types/role.go
index 5910f2f47..5cc0d3a56 100644
--- a/components/sdk-go/types/role.go
+++ b/components/sdk-go/types/role.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/role_binding.go b/components/sdk-go/types/role_binding.go
index 9b3c81cf5..95d8295ef 100644
--- a/components/sdk-go/types/role_binding.go
+++ b/components/sdk-go/types/role_binding.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/sandbox_template.go b/components/sdk-go/types/sandbox_template.go
index dbc4f56a9..5ceb3a3c0 100644
--- a/components/sdk-go/types/sandbox_template.go
+++ b/components/sdk-go/types/sandbox_template.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/secret_source.go b/components/sdk-go/types/secret_source.go
index b31935435..038e33b40 100644
--- a/components/sdk-go/types/secret_source.go
+++ b/components/sdk-go/types/secret_source.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-go/types/user.go b/components/sdk-go/types/user.go
index e06fb898b..a1dda713c 100644
--- a/components/sdk-go/types/user.go
+++ b/components/sdk-go/types/user.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
package types
diff --git a/components/sdk-typescript/src/agent_runtime.ts b/components/sdk-typescript/src/agent_runtime.ts
index af222decf..ef27d7ebc 100644
--- a/components/sdk-typescript/src/agent_runtime.ts
+++ b/components/sdk-typescript/src/agent_runtime.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/agent_runtime_api.ts b/components/sdk-typescript/src/agent_runtime_api.ts
index edb7f37e7..f97f71dbb 100644
--- a/components/sdk-typescript/src/agent_runtime_api.ts
+++ b/components/sdk-typescript/src/agent_runtime_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/base.ts b/components/sdk-typescript/src/base.ts
index 935513a51..96aa37942 100644
--- a/components/sdk-typescript/src/base.ts
+++ b/components/sdk-typescript/src/base.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
export type ObjectReference = {
id: string;
diff --git a/components/sdk-typescript/src/client.ts b/components/sdk-typescript/src/client.ts
index 453f4104a..431865a92 100644
--- a/components/sdk-typescript/src/client.ts
+++ b/components/sdk-typescript/src/client.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig } from './base.js';
import { AgentRuntimeAPI } from './agent_runtime_api.js';
diff --git a/components/sdk-typescript/src/gateway.ts b/components/sdk-typescript/src/gateway.ts
index 454a962a7..a66135239 100644
--- a/components/sdk-typescript/src/gateway.ts
+++ b/components/sdk-typescript/src/gateway.ts
@@ -1,8 +1,26 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
+export type GatewayPlacementAvailability = {
+ aws_public: boolean;
+ aws_reason?: "no-eligible-cluster";
+ aws_vpn: boolean;
+ ibm_public: boolean;
+ ibm_reason?: "no-eligible-cluster";
+ ibm_vpn: boolean;
+ local_kind: boolean;
+};
+
+
+
+export type GatewayPlacementIntent =
+ | { mode: 'local-kind' }
+ | { network: 'public'; provider: 'aws' | 'ibm' }
+ | { network: 'vpn'; provider: 'aws' };
+
+
export type Gateway = ObjectReference & {
active_sandbox_count: number;
@@ -34,12 +52,13 @@ export type GatewayList = ListMeta & {
};
export type GatewayCreateRequest = {
- cluster_id: string;
credential_driver?: string;
external_dns?: string;
image?: string;
name: string;
+ oidc?: string;
phase?: string;
+ placement: GatewayPlacementIntent;
route?: string;
server_dns_names?: string;
service_type?: string;
@@ -69,11 +88,6 @@ export class GatewayBuilder {
private data: Record = {};
- clusterId(value: string): this {
- this.data['cluster_id'] = value;
- return this;
- }
-
credentialDriver(value: string): this {
this.data['credential_driver'] = value;
return this;
@@ -94,11 +108,21 @@ export class GatewayBuilder {
return this;
}
+ oidc(value: string): this {
+ this.data['oidc'] = value;
+ return this;
+ }
+
phase(value: string): this {
this.data['phase'] = value;
return this;
}
+ placement(value: GatewayPlacementIntent): this {
+ this.data['placement'] = value;
+ return this;
+ }
+
route(value: string): this {
this.data['route'] = value;
return this;
@@ -130,14 +154,11 @@ export class GatewayBuilder {
}
build(): GatewayCreateRequest {
- if (!this.data['cluster_id']) {
- throw new Error('cluster_id is required');
- }
if (!this.data['name']) {
throw new Error('name is required');
}
- if (!this.data['namespace']) {
- throw new Error('namespace is required');
+ if (this.data['placement'] === undefined) {
+ throw new Error('placement is required');
}
return this.data as GatewayCreateRequest;
}
diff --git a/components/sdk-typescript/src/gateway_api.ts b/components/sdk-typescript/src/gateway_api.ts
index 6e11ebce7..f2cccc54e 100644
--- a/components/sdk-typescript/src/gateway_api.ts
+++ b/components/sdk-typescript/src/gateway_api.ts
@@ -1,14 +1,18 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
-import type { Gateway, GatewayList, GatewayCreateRequest, GatewayPatchRequest } from './gateway.js';
+import type { Gateway, GatewayList, GatewayCreateRequest, GatewayPatchRequest, GatewayPlacementAvailability } from './gateway.js';
export class GatewayAPI {
constructor(private readonly config: SDKClientConfig) {}
+ async placementAvailability(opts?: RequestOptions): Promise {
+ return sdkFetch(this.config, 'GET', '/gateways/placement-availability', undefined, opts);
+ }
+
async create(data: GatewayCreateRequest, opts?: RequestOptions): Promise {
return sdkFetch(this.config, 'POST', '/api/hypershell/v1/gateways', data, opts);
}
diff --git a/components/sdk-typescript/src/index.ts b/components/sdk-typescript/src/index.ts
index 1d95b5174..06a157a61 100644
--- a/components/sdk-typescript/src/index.ts
+++ b/components/sdk-typescript/src/index.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
export { SDKClient } from './client.js';
export type { SDKClientConfig, ListOptions, RequestOptions, ObjectReference, ListMeta, APIError } from './base.js';
diff --git a/components/sdk-typescript/src/inference_route.ts b/components/sdk-typescript/src/inference_route.ts
index cec713235..2c118611e 100644
--- a/components/sdk-typescript/src/inference_route.ts
+++ b/components/sdk-typescript/src/inference_route.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/inference_route_api.ts b/components/sdk-typescript/src/inference_route_api.ts
index ae85328c6..07d3509b0 100644
--- a/components/sdk-typescript/src/inference_route_api.ts
+++ b/components/sdk-typescript/src/inference_route_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/managed_cluster.ts b/components/sdk-typescript/src/managed_cluster.ts
index 806e55f49..2f30efb90 100644
--- a/components/sdk-typescript/src/managed_cluster.ts
+++ b/components/sdk-typescript/src/managed_cluster.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
@@ -13,6 +13,7 @@ export type ManagedCluster = ObjectReference & {
provider: string;
region: string;
status: string;
+ visibility: string;
};
export type ManagedClusterList = ListMeta & {
diff --git a/components/sdk-typescript/src/managed_cluster_api.ts b/components/sdk-typescript/src/managed_cluster_api.ts
index 4d91f096c..f8ba2c219 100644
--- a/components/sdk-typescript/src/managed_cluster_api.ts
+++ b/components/sdk-typescript/src/managed_cluster_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/open_shell_gateway_service_account.ts b/components/sdk-typescript/src/open_shell_gateway_service_account.ts
index 4ed91fa5c..b9768c44b 100644
--- a/components/sdk-typescript/src/open_shell_gateway_service_account.ts
+++ b/components/sdk-typescript/src/open_shell_gateway_service_account.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
export type OpenShellGatewayServiceAccountCapabilities = {
diff --git a/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts b/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts
index ae2a0fdbe..e1628afb0 100644
--- a/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts
+++ b/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, RequestOptions } from './base.js';
import { sdkFetch } from './base.js';
@@ -17,11 +17,11 @@ export class OpenShellGatewayServiceAccountAPI {
constructor(private readonly config: SDKClientConfig) {}
async create(gatewayId: string, data: OpenShellGatewayServiceAccountCreateRequest, opts?: RequestOptions): Promise {
- return sdkFetch(this.config, 'POST', `/api/hypershell/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts`, data, opts);
+ return sdkFetch(this.config, 'POST', `/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts`, data, opts);
}
async get(gatewayId: string, serviceAccountId: string, opts?: RequestOptions): Promise {
- return sdkFetch(this.config, 'GET', `/api/hypershell/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts/${encodeURIComponent(serviceAccountId)}`, undefined, opts);
+ return sdkFetch(this.config, 'GET', `/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts/${encodeURIComponent(serviceAccountId)}`, undefined, opts);
}
async list(gatewayId: string, listOpts?: OpenShellGatewayServiceAccountListOptions, opts?: RequestOptions): Promise {
@@ -45,16 +45,16 @@ export class OpenShellGatewayServiceAccountAPI {
params.set('order', String(listOpts.order));
}
const query = params.toString();
- const path = `/api/hypershell/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts` + (query ? `?${query}` : '');
+ const path = `/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts` + (query ? `?${query}` : '');
return sdkFetch(this.config, 'GET', path, undefined, opts);
}
async revoke(gatewayId: string, serviceAccountId: string, opts?: RequestOptions): Promise {
- return sdkFetch(this.config, 'POST', `/api/hypershell/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts/${encodeURIComponent(serviceAccountId)}` + '/revoke', undefined, opts);
+ return sdkFetch(this.config, 'POST', `/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts/${encodeURIComponent(serviceAccountId)}` + '/revoke', undefined, opts);
}
async delete(gatewayId: string, serviceAccountId: string, opts?: RequestOptions): Promise {
- return sdkFetch(this.config, 'DELETE', `/api/hypershell/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts/${encodeURIComponent(serviceAccountId)}`, undefined, opts);
+ return sdkFetch(this.config, 'DELETE', `/v1/gateways/${encodeURIComponent(gatewayId)}/service_accounts/${encodeURIComponent(serviceAccountId)}`, undefined, opts);
}
async *listAll(gatewayId: string, size: number = 20, opts?: RequestOptions): AsyncGenerator {
diff --git a/components/sdk-typescript/src/provider_binding.ts b/components/sdk-typescript/src/provider_binding.ts
index 71eb178ec..99b100649 100644
--- a/components/sdk-typescript/src/provider_binding.ts
+++ b/components/sdk-typescript/src/provider_binding.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/provider_binding_api.ts b/components/sdk-typescript/src/provider_binding_api.ts
index 5ea7cd252..cfc28fbce 100644
--- a/components/sdk-typescript/src/provider_binding_api.ts
+++ b/components/sdk-typescript/src/provider_binding_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/provider_spec.ts b/components/sdk-typescript/src/provider_spec.ts
index e0183c27f..20898fd8c 100644
--- a/components/sdk-typescript/src/provider_spec.ts
+++ b/components/sdk-typescript/src/provider_spec.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/provider_spec_api.ts b/components/sdk-typescript/src/provider_spec_api.ts
index 500655296..57885c894 100644
--- a/components/sdk-typescript/src/provider_spec_api.ts
+++ b/components/sdk-typescript/src/provider_spec_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/role.ts b/components/sdk-typescript/src/role.ts
index 548ad0e03..ee0a8d75a 100644
--- a/components/sdk-typescript/src/role.ts
+++ b/components/sdk-typescript/src/role.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/role_api.ts b/components/sdk-typescript/src/role_api.ts
index be292b9c8..f6b4fe72e 100644
--- a/components/sdk-typescript/src/role_api.ts
+++ b/components/sdk-typescript/src/role_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/role_binding.ts b/components/sdk-typescript/src/role_binding.ts
index a325243eb..3a7c4f585 100644
--- a/components/sdk-typescript/src/role_binding.ts
+++ b/components/sdk-typescript/src/role_binding.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/role_binding_api.ts b/components/sdk-typescript/src/role_binding_api.ts
index e26c10472..9a581f03d 100644
--- a/components/sdk-typescript/src/role_binding_api.ts
+++ b/components/sdk-typescript/src/role_binding_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/sandbox_template.ts b/components/sdk-typescript/src/sandbox_template.ts
index 2718f0bd8..d7e3b9b9f 100644
--- a/components/sdk-typescript/src/sandbox_template.ts
+++ b/components/sdk-typescript/src/sandbox_template.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/sandbox_template_api.ts b/components/sdk-typescript/src/sandbox_template_api.ts
index a6641b447..5fb1b3b45 100644
--- a/components/sdk-typescript/src/sandbox_template_api.ts
+++ b/components/sdk-typescript/src/sandbox_template_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/secret_source.ts b/components/sdk-typescript/src/secret_source.ts
index 0e545cc16..ae6ec88ee 100644
--- a/components/sdk-typescript/src/secret_source.ts
+++ b/components/sdk-typescript/src/secret_source.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/secret_source_api.ts b/components/sdk-typescript/src/secret_source_api.ts
index cb051300b..9f6b98fa9 100644
--- a/components/sdk-typescript/src/secret_source_api.ts
+++ b/components/sdk-typescript/src/secret_source_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/user.ts b/components/sdk-typescript/src/user.ts
index 4fb6ff4ef..0b04f9665 100644
--- a/components/sdk-typescript/src/user.ts
+++ b/components/sdk-typescript/src/user.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/user_api.ts b/components/sdk-typescript/src/user_api.ts
index 580c44195..5a935147d 100644
--- a/components/sdk-typescript/src/user_api.ts
+++ b/components/sdk-typescript/src/user_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: e6bcd9eeb2b0f1844ca3cefb9576ac8b1acb78c6d19e0a83166f2a3c8acfdbd2
+// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/web-console/app/adapters/api/gateway-operations.test.ts b/components/web-console/app/adapters/api/gateway-operations.test.ts
index 0d0cdf82b..45c97d082 100644
--- a/components/web-console/app/adapters/api/gateway-operations.test.ts
+++ b/components/web-console/app/adapters/api/gateway-operations.test.ts
@@ -18,6 +18,7 @@ const gatewayApi = {
delete: vi.fn(),
get: vi.fn(),
list: vi.fn(),
+ placementAvailability: vi.fn(),
update: vi.fn(),
};
const managedClusterApi = {
@@ -112,6 +113,7 @@ function managedCluster(
region: "us-east-1",
status: "Ready",
updated_at: null,
+ visibility: "public",
...overrides,
};
}
@@ -666,27 +668,55 @@ describe("gateway API operations adapter", () => {
);
});
- it("provisions on the selected cluster with hidden request defaults", async () => {
+ it("provisions from placement intent with hidden request defaults", async () => {
gatewayApi.create.mockResolvedValue(gateway({}));
await controlPlane.provisionGateway(
{
- clusterId: "cluster-east",
name: "team-gateway",
+ placement: { network: "public", provider: "aws" },
},
context,
);
expect(gatewayApi.create).toHaveBeenCalledWith(
- {
- cluster_id: "cluster-east",
- name: "team-gateway",
- route: '{"enabled":true}',
+ {
+ name: "team-gateway",
+ placement: { network: "public", provider: "aws" },
+ route: '{"enabled":true}',
},
{ signal: undefined },
);
});
+ it("maps backend placement availability", async () => {
+ gatewayApi.placementAvailability.mockResolvedValue({
+ aws_public: true,
+ aws_vpn: true,
+ ibm_public: false,
+ ibm_vpn: false,
+ local_kind: true,
+ });
+
+ if (controlPlane.getGatewayPlacementAvailability === undefined) {
+ throw new Error("placement availability operation is unavailable");
+ }
+ await expect(
+ controlPlane.getGatewayPlacementAvailability(context),
+ ).resolves.toEqual({
+ awsPublic: true,
+ awsVpn: true,
+ awsReason: undefined,
+ ibmPublic: false,
+ ibmVpn: false,
+ ibmReason: undefined,
+ localKind: true,
+ });
+ expect(gatewayApi.placementAvailability).toHaveBeenCalledWith({
+ signal: undefined,
+ });
+ });
+
it("maps detail, rename, and deletion operations", async () => {
gatewayApi.get.mockResolvedValue(gateway());
gatewayApi.update.mockResolvedValue(gateway({ name: "Renamed gateway" }));
diff --git a/components/web-console/app/adapters/api/gateway-operations.ts b/components/web-console/app/adapters/api/gateway-operations.ts
index 3520371e6..d047cb9ac 100644
--- a/components/web-console/app/adapters/api/gateway-operations.ts
+++ b/components/web-console/app/adapters/api/gateway-operations.ts
@@ -5,6 +5,7 @@ import type {
GatewayInvocationContext,
GatewayListRequest,
GatewayPlacement,
+ GatewayPlacementAvailability,
GatewayRecord,
OpenShellGatewayServiceAccountCapabilities,
OpenShellGatewayServiceAccountConnection,
@@ -34,7 +35,8 @@ import {
type GatewayApi = Pick<
SDKClient["gateways"],
"create" | "delete" | "get" | "list" | "update"
->;
+> &
+ Partial>;
type ManagedClusterApi = Pick;
type ServiceAccountApi = Pick<
SDKClient["openShellGatewayServiceAccounts"],
@@ -395,6 +397,26 @@ export function createGatewayControlPlaneAdapter(
};
});
},
+ async getGatewayPlacementAvailability(context) {
+ return mapFailure(async () => {
+ const gatewaysApi = apiClient(apiFactory, context).gateways;
+ if (!gatewaysApi.placementAvailability) {
+ throw new Error("placement availability API is unavailable");
+ }
+ const availability = await gatewaysApi.placementAvailability({
+ signal: context.signal,
+ });
+ return {
+ awsPublic: availability.aws_public,
+ awsVpn: availability.aws_vpn,
+ awsReason: availability.aws_reason,
+ ibmPublic: availability.ibm_public,
+ ibmVpn: availability.ibm_vpn,
+ ibmReason: availability.ibm_reason,
+ localKind: availability.local_kind,
+ } satisfies GatewayPlacementAvailability;
+ });
+ },
async getGatewayPlacement(clusterId, context) {
return mapFailure(async () =>
toGatewayPlacement(
@@ -541,13 +563,19 @@ export function createGatewayControlPlaneAdapter(
});
},
async provisionGateway(input, context) {
+ if (input.placement === undefined) {
+ throw new GatewayOperationError("unknown", {
+ cause: new Error("gateway placement intent is required"),
+ });
+ }
+ const placement = input.placement;
return mapFailure(async () =>
toGatewayRecord(
await apiClient(apiFactory, context).gateways.create(
- {
- cluster_id: input.clusterId,
- name: input.name,
- route: JSON.stringify({ enabled: true }),
+ {
+ name: input.name,
+ placement,
+ route: JSON.stringify({ enabled: true }),
},
{ signal: context.signal },
),
diff --git a/components/web-console/app/features/gateways/gateway-create.stories.tsx b/components/web-console/app/features/gateways/gateway-create.stories.tsx
new file mode 100644
index 000000000..9bd8b73b2
--- /dev/null
+++ b/components/web-console/app/features/gateways/gateway-create.stories.tsx
@@ -0,0 +1,79 @@
+import type {
+ GatewayOperations,
+ GatewayRecord,
+ GatewayUiNavigation,
+} from "@openshift-online/hypershell-gateway-management-ui";
+import {
+ GatewayCreatePage,
+ GatewayUiProvider,
+} from "@openshift-online/hypershell-gateway-management-ui";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { expect, userEvent, within } from "storybook/test";
+
+const placementAvailability = {
+ awsPublic: true,
+ awsVpn: true,
+ ibmPublic: true,
+ ibmVpn: false,
+ localKind: true,
+} as const;
+
+const previewGateway: GatewayRecord = {
+ clusterId: "preview-cluster",
+ id: "preview-gateway",
+ name: "preview-gateway",
+ namespace: "openshell",
+ status: "Provisioning",
+};
+
+const gatewayOperations = {
+ getGatewayPlacementAvailability: () => Promise.resolve(placementAvailability),
+ provisionGateway: () => Promise.resolve(previewGateway),
+} as unknown as GatewayOperations;
+
+const navigation: GatewayUiNavigation = {
+ collectionHref: "/gateways",
+ createHref: "/gateways/new",
+ detailHref: (gatewayId) => `/gateways/${gatewayId}`,
+ navigate: () => undefined,
+};
+
+function GatewayCreatePreview() {
+ return (
+
+
+
+ );
+}
+
+const meta = {
+ title: "HyperShell/Gateways/Create gateway",
+ component: GatewayCreatePage,
+ parameters: {
+ layout: "fullscreen",
+ },
+ render: () => ,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const ManagedPlacementsAvailable: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await expect(
+ canvas.queryByRole("radio", { name: "Use local-kind" }),
+ ).toBeNull();
+ await expect(canvas.getByRole("radio", { name: "Public" })).toBeVisible();
+ await expect(canvas.getByRole("radio", { name: "VPN" })).toBeVisible();
+ await expect(canvas.queryByRole("radio", { name: "AWS" })).toBeNull();
+ await expect(canvas.queryByRole("radio", { name: "IBM Cloud" })).toBeNull();
+
+ await userEvent.click(canvas.getByRole("radio", { name: "Public" }));
+ await expect(canvas.getByRole("radio", { name: "AWS" })).toBeVisible();
+ await expect(
+ canvas.getByRole("radio", { name: "IBM Cloud" }),
+ ).toBeVisible();
+ },
+};
diff --git a/components/web-console/locales/en.json b/components/web-console/locales/en.json
index 53c4c60db..49117e520 100644
--- a/components/web-console/locales/en.json
+++ b/components/web-console/locales/en.json
@@ -1519,6 +1519,14 @@
"defaultMessage": "Gateway details",
"description": "Metadata title for a user-facing gateway details page."
},
+ "app.page.gatewayProvision.awsProvider": {
+ "defaultMessage": "AWS",
+ "description": "AWS cloud provider option."
+ },
+ "app.page.gatewayProvision.cloudProvider": {
+ "defaultMessage": "Cloud provider",
+ "description": "Cloud provider for public gateway placement."
+ },
"app.page.gatewayProvision.description": {
"defaultMessage": "Configure a new OpenShell gateway.",
"description": "Supporting text on the gateway provisioning page."
@@ -1531,14 +1539,78 @@
"defaultMessage": "Gateway could not be provisioned",
"description": "Title shown when gateway provisioning fails."
},
+ "app.page.gatewayProvision.ibmCloudProvider": {
+ "defaultMessage": "IBM Cloud",
+ "description": "IBM Cloud provider option."
+ },
+ "app.page.gatewayProvision.localDevelopment": {
+ "defaultMessage": "Local development",
+ "description": "Form group label for local Kind placement."
+ },
+ "app.page.gatewayProvision.localKindPlacement": {
+ "defaultMessage": "Use local-kind",
+ "description": "Local Kind placement option."
+ },
+ "app.page.gatewayProvision.localKindPlacementDescription": {
+ "defaultMessage": "Use the local Kind cluster for development.",
+ "description": "Description of local Kind placement."
+ },
+ "app.page.gatewayProvision.networkAccess": {
+ "defaultMessage": "Network access",
+ "description": "Network exposure mode for a gateway."
+ },
+ "app.page.gatewayProvision.noEligibleCluster": {
+ "defaultMessage": "no eligible managed cluster is connected.",
+ "description": "Provider unavailability reason when no matching connected cluster exists."
+ },
+ "app.page.gatewayProvision.noManagedPlacement": {
+ "defaultMessage": "No managed placement is currently available.",
+ "description": "Warning shown when no provider or local development placement can be selected."
+ },
"app.page.gatewayProvision.pending": {
"defaultMessage": "Provisioning gateway",
"description": "Accessible progress text while a gateway is provisioning."
},
+ "app.page.gatewayProvision.placementAvailabilityError": {
+ "defaultMessage": "Placement availability could not be loaded. Cloud placement is disabled until availability can be verified.",
+ "description": "Warning shown when current gateway placement availability cannot be retrieved."
+ },
+ "app.page.gatewayProvision.placementHelp": {
+ "defaultMessage": "A matching managed cluster is selected at random for the chosen network and provider.",
+ "description": "Explanation of automatic gateway placement."
+ },
+ "app.page.gatewayProvision.publicNetwork": {
+ "defaultMessage": "Public",
+ "description": "Public network placement option."
+ },
+ "app.page.gatewayProvision.publicNetworkDescription": {
+ "defaultMessage": "Accessible through a public endpoint. Choose a cloud provider below.",
+ "description": "Description of public network placement."
+ },
"app.page.gatewayProvision.title": {
"defaultMessage": "Provision gateway",
"description": "Page title and action for provisioning a gateway."
},
+ "app.page.gatewayProvision.unavailableNetwork": {
+ "defaultMessage": "{network} is unavailable: {reason}",
+ "description": "Explanation shown when a network cannot currently accept placement."
+ },
+ "app.page.gatewayProvision.unavailableProvider": {
+ "defaultMessage": "{provider} is unavailable: {reason}",
+ "description": "Explanation shown when a provider cannot currently accept placement."
+ },
+ "app.page.gatewayProvision.vpnNetwork": {
+ "defaultMessage": "VPN",
+ "description": "VPN network placement option."
+ },
+ "app.page.gatewayProvision.vpnNetworkDescription": {
+ "defaultMessage": "Private network access. AWS is required for VPN placement.",
+ "description": "Description of VPN placement."
+ },
+ "app.page.gatewayProvision.vpnRequiresAws": {
+ "defaultMessage": "VPN placement requires AWS.",
+ "description": "Explanation shown when IBM Cloud is disabled for VPN."
+ },
"app.page.gateways.description": {
"defaultMessage": "HyperShell gateways.",
"description": "Browser metadata description for the gateways page."
diff --git a/deploy/base/applications/api-server.yaml b/deploy/base/applications/api-server.yaml
index 11f4cd3c4..3103945eb 100644
--- a/deploy/base/applications/api-server.yaml
+++ b/deploy/base/applications/api-server.yaml
@@ -104,6 +104,8 @@ spec:
value: "hypershell-controller.$(POD_NAMESPACE).svc.cluster.local:9443"
- name: DB_SSLMODE
value: require
+ - name: HYPERSHELL_PLATFORM_KIND
+ value: "false"
startupProbe:
httpGet:
path: /healthcheck
diff --git a/deploy/base/control-plane/deployment.yaml b/deploy/base/control-plane/deployment.yaml
index 6b2b579c4..939b62547 100644
--- a/deploy/base/control-plane/deployment.yaml
+++ b/deploy/base/control-plane/deployment.yaml
@@ -28,6 +28,10 @@ spec:
value: "hypershell-api-server:9000"
- name: HYPERSHELL_API_SERVER_URL
value: "http://hypershell-api-server:8000"
+ - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
+ value: "aws"
+ - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
+ value: "public"
- name: HYPERSHELL_NAMESPACE
value: "hypershell-stage"
- name: HYPERSHELL_LOG_LEVEL
diff --git a/deploy/base/platform-resources/controller.yaml b/deploy/base/platform-resources/controller.yaml
index 94bb40cb1..4910822c4 100644
--- a/deploy/base/platform-resources/controller.yaml
+++ b/deploy/base/platform-resources/controller.yaml
@@ -60,6 +60,10 @@ spec:
value: "hypershell-api-server:9000"
- name: HYPERSHELL_API_SERVER_URL
value: "http://hypershell-api-server:8000"
+ - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
+ value: "aws"
+ - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
+ value: "public"
- name: HYPERSHELL_NAMESPACE
valueFrom:
fieldRef:
diff --git a/deploy/gitops/platform/kustomization.yaml b/deploy/gitops/platform/kustomization.yaml
index 3a1a12b59..289a655e7 100644
--- a/deploy/gitops/platform/kustomization.yaml
+++ b/deploy/gitops/platform/kustomization.yaml
@@ -29,3 +29,7 @@ patches:
env:
- name: HYPERSHELL_MANAGED_CLUSTER_NAME
value: "hub"
+ - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
+ value: "aws"
+ - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
+ value: "public"
diff --git a/deploy/ibm/kustomization.yaml b/deploy/ibm/kustomization.yaml
index a17cf4ef8..99a6fcde2 100644
--- a/deploy/ibm/kustomization.yaml
+++ b/deploy/ibm/kustomization.yaml
@@ -64,6 +64,10 @@ patches:
containers:
- name: controller
env:
+ - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
+ value: "ibm"
+ - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
+ value: "public"
- name: GATEWAY_INGRESS_MODE
value: route
- name: GATEWAY_API_BASE_DOMAIN
diff --git a/deploy/kind/kustomization.yaml b/deploy/kind/kustomization.yaml
index 16b2b9549..18aa0f11b 100644
--- a/deploy/kind/kustomization.yaml
+++ b/deploy/kind/kustomization.yaml
@@ -67,6 +67,10 @@ patches:
# creating it (managed-cluster-registration.spec.md).
- name: HYPERSHELL_MANAGED_CLUSTER_NAME
value: "local-kind"
+ - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
+ value: "kind"
+ - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
+ value: "public"
- name: OIDC_ISSUER
value: "http://keycloak-service.keycloak.svc.cluster.local:8080/realms/hypershell"
- name: OIDC_CLIENT_ID
@@ -213,6 +217,31 @@ patches:
value: "true"
- name: RBAC_SERVICE_ACCOUNTS
value: "service-account-hypershell-control-plane"
+ - name: HYPERSHELL_PLATFORM_KIND
+ value: "true"
+ # --- Keycloak: fixed HTTPS issuer reachable in-cluster and from host ---
+ # The token `iss` is derived from KC_HOSTNAME. Gateways validate tokens by
+ # requiring config.issuer == discovery.issuer == token.iss, so the issuer
+ # must be a single URL reachable over HTTPS both in-cluster (via CoreDNS ->
+ # gateway LB) and from the host (via the gateway LB :443). The LB terminates
+ # TLS with the *.hypershell.localhost cert and forwards to keycloak-service:8080;
+ # Keycloak stays plain HTTP internally and honors X-Forwarded-Proto
+ # (KC_PROXY_HEADERS=xforwarded). This value matches E2E_OIDC_ISSUER and the
+ # seeded gateway OIDC issuer.
+ - patch: |
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: keycloak
+ namespace: keycloak
+ spec:
+ template:
+ spec:
+ containers:
+ - name: keycloak
+ env:
+ - name: KC_HOSTNAME
+ value: "https://keycloak.hypershell.localhost"
- target:
kind: Deployment
name: hypershell-api-server
diff --git a/deploy/openshift/kustomization.yaml b/deploy/openshift/kustomization.yaml
index 251ef0448..7973e479c 100644
--- a/deploy/openshift/kustomization.yaml
+++ b/deploy/openshift/kustomization.yaml
@@ -53,6 +53,10 @@ patches:
# instead of creating it (managed-cluster-registration.spec.md).
- name: HYPERSHELL_MANAGED_CLUSTER_NAME
value: "local-openshift"
+ - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
+ value: "aws"
+ - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
+ value: "public"
- name: OIDC_ISSUER
valueFrom:
secretKeyRef:
diff --git a/packages/gateway-management-ui/src/application/gateway-operations.ts b/packages/gateway-management-ui/src/application/gateway-operations.ts
index 08e5f090d..1fe538077 100644
--- a/packages/gateway-management-ui/src/application/gateway-operations.ts
+++ b/packages/gateway-management-ui/src/application/gateway-operations.ts
@@ -200,6 +200,15 @@ export function createGatewayOperations({
execute("find-placements", signal, (context) =>
controlPlane.findGatewayPlacements(search.trim(), context),
),
+ getGatewayPlacementAvailability: (signal) =>
+ execute("placement-availability", signal, (context) => {
+ if (!controlPlane.getGatewayPlacementAvailability) {
+ throw new GatewayOperationError("unknown", {
+ cause: new Error("placement availability is unavailable"),
+ });
+ }
+ return controlPlane.getGatewayPlacementAvailability(context);
+ }),
getGatewayPlacement: (clusterId, signal) =>
execute("get-placement", signal, (context) =>
controlPlane.getGatewayPlacement(clusterId, context),
diff --git a/packages/gateway-management-ui/src/application/gateway-probes.ts b/packages/gateway-management-ui/src/application/gateway-probes.ts
index b80388b57..d0480b47e 100644
--- a/packages/gateway-management-ui/src/application/gateway-probes.ts
+++ b/packages/gateway-management-ui/src/application/gateway-probes.ts
@@ -9,6 +9,7 @@ export type GatewayAction =
| "create-service-account"
| "delete-service-account"
| "find-placements"
+ | "placement-availability"
| "get"
| "get-placement"
| "get-placements"
diff --git a/packages/gateway-management-ui/src/application/gateway-types.ts b/packages/gateway-management-ui/src/application/gateway-types.ts
index 35c0ac1d3..48d389ae6 100644
--- a/packages/gateway-management-ui/src/application/gateway-types.ts
+++ b/packages/gateway-management-ui/src/application/gateway-types.ts
@@ -81,8 +81,29 @@ export interface GatewayInvocationContext {
}
export interface GatewayProvisionInput {
- clusterId: string;
name: string;
+ placement?: GatewayPlacementIntent;
+ /** @deprecated Kept only for source compatibility with older adapters. */
+ clusterId?: string;
+}
+
+export type GatewayProvider = "aws" | "ibm";
+
+export type GatewayPlacementIntent =
+ | { mode: "local-kind" }
+ | { network: "public"; provider: GatewayProvider }
+ | { network: "vpn"; provider: "aws" };
+
+export type GatewayPlacementUnavailableReason = "no-eligible-cluster";
+
+export interface GatewayPlacementAvailability {
+ awsPublic: boolean;
+ awsVpn: boolean;
+ awsReason?: GatewayPlacementUnavailableReason;
+ ibmPublic: boolean;
+ ibmVpn: boolean;
+ ibmReason?: GatewayPlacementUnavailableReason;
+ localKind: boolean;
}
export type OpenShellGatewayServiceAccountRole =
@@ -233,6 +254,9 @@ export interface GatewayControlPlane {
search: string,
context: GatewayInvocationContext,
): Promise;
+ getGatewayPlacementAvailability?(
+ context: GatewayInvocationContext,
+ ): Promise;
getGatewayPlacement(
clusterId: string,
context: GatewayInvocationContext,
@@ -295,6 +319,9 @@ export interface GatewayOperations {
search: string,
signal?: AbortSignal,
): Promise;
+ getGatewayPlacementAvailability?(
+ signal?: AbortSignal,
+ ): Promise;
getGatewayPlacement(
clusterId: string,
signal?: AbortSignal,
diff --git a/packages/gateway-management-ui/src/assets/aws-logo.svg b/packages/gateway-management-ui/src/assets/aws-logo.svg
new file mode 100644
index 000000000..80192a12d
--- /dev/null
+++ b/packages/gateway-management-ui/src/assets/aws-logo.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/gateway-management-ui/src/assets/ibm-cloud.svg b/packages/gateway-management-ui/src/assets/ibm-cloud.svg
new file mode 100644
index 000000000..5f704cc3b
--- /dev/null
+++ b/packages/gateway-management-ui/src/assets/ibm-cloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.module.css b/packages/gateway-management-ui/src/gateways/gateway-create.module.css
new file mode 100644
index 000000000..69176d685
--- /dev/null
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.module.css
@@ -0,0 +1,22 @@
+.choice {
+ min-height: 7rem;
+}
+.choiceTitle {
+ font-weight: 700;
+ font-size: 1.1rem;
+}
+.choiceHeading {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ min-height: 3rem;
+}
+.providerLogo {
+ width: 7rem;
+ height: 3rem;
+ object-fit: contain;
+ object-position: left center;
+}
+.help {
+ margin-top: 1.5rem;
+}
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
index b00740962..c908658fc 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
@@ -2,62 +2,57 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { IntlProvider } from "react-intl";
-import { vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
import { GatewayUiProvider } from "../gateway-ui-provider";
import { GatewayCreatePage } from "./gateway-create";
-const { createGatewayMock, findGatewayPlacementsMock, navigateMock } =
- vi.hoisted(() => ({
- createGatewayMock: vi.fn(),
- findGatewayPlacementsMock: vi.fn(),
- navigateMock: vi.fn(),
- }));
+const { provisionGateway, navigate } = vi.hoisted(() => ({
+ provisionGateway: vi.fn(),
+ navigate: vi.fn(),
+}));
-const gatewayOperations = {
+const availablePlacement = {
+ awsPublic: true,
+ awsVpn: true,
+ ibmPublic: true,
+ ibmVpn: false,
+ localKind: false,
+} as const;
+
+const operations = {
createOpenShellGatewayServiceAccount: vi.fn(),
deleteOpenShellGatewayServiceAccount: vi.fn(),
- findGatewayPlacements: findGatewayPlacementsMock,
+ findGatewayPlacements: vi.fn(),
getGateway: vi.fn(),
getGatewayPlacement: vi.fn(),
+ getGatewayPlacementAvailability: vi.fn(),
getGatewayPlacements: vi.fn(),
getOpenShellGatewayServiceAccount: vi.fn(),
listGateways: vi.fn(),
listOpenShellGatewayServiceAccounts: vi.fn(),
- provisionGateway: createGatewayMock,
+ provisionGateway,
removeGateway: vi.fn(),
renameGateway: vi.fn(),
revokeOpenShellGatewayServiceAccount: vi.fn(),
};
-const navigation = {
- collectionHref: "/",
- createHref: "/gateways/new",
- detailHref: (gatewayId: string) => `/gateways/${gatewayId}`,
- navigate: navigateMock,
-};
-
-const createdGateway = {
- clusterId: "",
- externalDns: "",
- id: "gateway-1",
- name: "team-gateway",
- namespace: "openshell",
- phase: "",
- status: "",
-};
-
-function createTestQueryClient() {
- return new QueryClient({
- defaultOptions: { mutations: { retry: false }, queries: { retry: false } },
+function renderPage() {
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
-}
-
-function renderPage(queryClient = createTestQueryClient()) {
return render(
-
-
+
+ `/gateways/${id}`,
+ navigate,
+ }}
+ >
@@ -65,433 +60,229 @@ function renderPage(queryClient = createTestQueryClient()) {
);
}
+async function enterNameAndSelectPublic(
+ user: ReturnType,
+) {
+ await user.type(
+ screen.getByRole("textbox", { name: "Gateway name" }),
+ "team-gateway",
+ );
+ await user.click(screen.getByRole("radio", { name: "Public" }));
+}
+
+function radio(name: string) {
+ return screen.getByRole("radio", { name });
+}
+
describe("GatewayCreatePage", () => {
beforeEach(() => {
vi.clearAllMocks();
- findGatewayPlacementsMock.mockResolvedValue({
- hasMore: false,
- items: [
- {
- id: "cluster-east",
- name: "Cluster East",
- provider: "AWS",
- region: "us-east-1",
- status: "Ready",
- },
- ],
+ operations.getGatewayPlacementAvailability.mockResolvedValue(
+ availablePlacement,
+ );
+ provisionGateway.mockResolvedValue({
+ id: "gateway-1",
+ name: "team-gateway",
});
- navigateMock.mockResolvedValue(undefined);
});
- it("preselects the only registered cluster without exposing a namespace", async () => {
+ it("defaults public placement to IBM Cloud", async () => {
const user = userEvent.setup();
- createGatewayMock.mockResolvedValue({
- ...createdGateway,
- clusterId: "cluster-east",
- });
renderPage();
+ await enterNameAndSelectPublic(user);
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
- });
- await waitFor(() => {
- expect(clusterInput.value).toBe("Cluster East");
- });
- expect(
- screen.getByText(
- "Only clusters with a connected control plane can host a gateway.",
- ),
- ).toBeTruthy();
- expect(screen.queryByLabelText("Namespace")).toBeNull();
- expect(screen.queryByLabelText("Gateway release")).toBeNull();
-
- await user.type(
- screen.getByRole("textbox", { name: "Gateway name" }),
- " team-gateway ",
- );
+ expect(radio("IBM Cloud").checked).toBe(true);
+ expect(radio("AWS").checked).toBe(false);
await user.click(screen.getByRole("button", { name: "Provision gateway" }));
await waitFor(() => {
- expect(createGatewayMock).toHaveBeenCalledWith({
- clusterId: "cluster-east",
+ expect(provisionGateway).toHaveBeenCalledWith({
name: "team-gateway",
+ placement: { network: "public", provider: "ibm" },
});
});
- await waitFor(() => {
- expect(navigateMock).toHaveBeenCalledWith("/gateways/gateway-1");
- });
});
- it("searches managed clusters and provisions on the selected result", async () => {
+ it("forces AWS for VPN and disables IBM Cloud", async () => {
const user = userEvent.setup();
- createGatewayMock.mockResolvedValue({
- ...createdGateway,
- clusterId: "cluster-east",
- });
renderPage();
-
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
- });
- await waitFor(() => {
- expect(clusterInput.value).toBe("Cluster East");
- });
- await user.click(
- screen.getByRole("button", { name: "Clear cluster search" }),
- );
- await user.type(clusterInput, "East");
- const clusterOption = await screen.findByText("Cluster East");
- expect(screen.getByText("Provider: AWS; region: us-east-1")).toBeTruthy();
- await user.click(clusterOption);
- expect(clusterInput.value).toBe("Cluster East");
-
await user.type(
screen.getByRole("textbox", { name: "Gateway name" }),
"team-gateway",
);
+ await user.click(screen.getByRole("radio", { name: "VPN" }));
+
+ expect(radio("AWS").checked).toBe(true);
+ expect(radio("IBM Cloud").disabled).toBe(true);
await user.click(screen.getByRole("button", { name: "Provision gateway" }));
await waitFor(() => {
- expect(createGatewayMock).toHaveBeenCalledWith({
- clusterId: "cluster-east",
+ expect(provisionGateway).toHaveBeenCalledWith({
name: "team-gateway",
+ placement: { network: "vpn", provider: "aws" },
});
});
- expect(findGatewayPlacementsMock).toHaveBeenCalledWith(
- "East",
- expect.any(AbortSignal),
- );
});
- it("debounces placement searches into one API request", async () => {
+ it("uses accessible radio groups with mutually exclusive choices", async () => {
const user = userEvent.setup();
renderPage();
- await waitFor(() => {
- expect(findGatewayPlacementsMock).toHaveBeenCalledWith(
- "",
- expect.any(AbortSignal),
- );
- });
- findGatewayPlacementsMock.mockClear();
-
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
- });
- await waitFor(() => {
- expect(clusterInput.value).toBe("Cluster East");
- });
- await user.click(
- screen.getByRole("button", { name: "Clear cluster search" }),
- );
- await user.type(clusterInput, "East");
-
- expect(findGatewayPlacementsMock).not.toHaveBeenCalled();
- await waitFor(() => {
- expect(findGatewayPlacementsMock).toHaveBeenCalledWith(
- "East",
- expect.any(AbortSignal),
- );
- });
- expect(findGatewayPlacementsMock).toHaveBeenCalledOnce();
- });
-
- it("shows one loading message while placement options are pending", async () => {
- const user = userEvent.setup();
- findGatewayPlacementsMock.mockReturnValue(new Promise(() => undefined));
- renderPage();
-
- const clusterInput = screen.getByRole("combobox", { name: "Cluster" });
- await user.click(clusterInput);
-
- expect(await screen.findAllByText("Loading managed clusters")).toHaveLength(
- 1,
- );
- expect(clusterInput.getAttribute("aria-busy")).toBe("true");
- });
-
- it("reuses fresh placement results after remounting", async () => {
- const user = userEvent.setup();
- const queryClient = createTestQueryClient();
- const firstRender = renderPage(queryClient);
- await user.click(screen.getByRole("combobox", { name: "Cluster" }));
- expect(await screen.findByText("Cluster East")).toBeTruthy();
- expect(findGatewayPlacementsMock).toHaveBeenCalledOnce();
- firstRender.unmount();
-
- renderPage(queryClient);
- await user.click(screen.getByRole("combobox", { name: "Cluster" }));
- expect(await screen.findByText("Cluster East")).toBeTruthy();
- await new Promise((resolve) => window.setTimeout(resolve, 50));
- expect(findGatewayPlacementsMock).toHaveBeenCalledOnce();
- });
-
- it("blocks provisioning when managed clusters fail to load", async () => {
- const user = userEvent.setup();
- findGatewayPlacementsMock.mockRejectedValue(
- new Error("managed cluster API unavailable"),
- );
- renderPage();
expect(
- await screen.findByText("Managed clusters could not be loaded"),
+ screen.getByRole("radiogroup", { name: "Network access" }),
).toBeTruthy();
- expect(
- screen.getByText(
- "A registered cluster is required to provision a gateway. Try loading managed clusters again.",
- ),
- ).toBeTruthy();
- expect(
- screen.getByRole("combobox", { name: "Cluster" }).value,
- ).toBe("");
- expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
-
- await user.type(
- screen.getByRole("textbox", { name: "Gateway name" }),
- "team-gateway",
- );
- await user.click(screen.getByRole("button", { name: "Provision gateway" }));
- expect(await screen.findByText("This field is required.")).toBeTruthy();
- expect(createGatewayMock).not.toHaveBeenCalled();
+ await user.click(screen.getByRole("radio", { name: "Public" }));
+ expect(radio("Public").checked).toBe(true);
+ await user.click(screen.getByRole("radio", { name: "VPN" }));
+ expect(radio("VPN").checked).toBe(true);
+ expect(radio("Public").checked).toBe(false);
});
- it("leaves the cluster unselected when several registered clusters exist", async () => {
+ it("shows required placement and name errors without disabling submit", async () => {
const user = userEvent.setup();
- findGatewayPlacementsMock.mockResolvedValue({
- hasMore: false,
- items: [
- { id: "cluster-east", name: "Cluster East", provider: "AWS" },
- { id: "cluster-west", name: "Cluster West", provider: "GCP" },
- ],
- });
renderPage();
+ const submit = screen.getByRole("button", { name: "Provision gateway" });
+ await user.click(submit);
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
- });
- await user.click(clusterInput);
- expect(await screen.findByText("Cluster East")).toBeTruthy();
- expect(screen.getByText("Cluster West")).toBeTruthy();
- expect(clusterInput.value).toBe("");
+ expect((submit as HTMLButtonElement).disabled).toBe(false);
+ expect(screen.getAllByText("This field is required.")).toHaveLength(2);
+ expect(provisionGateway).not.toHaveBeenCalled();
});
- it("does not override a field the user already touched with the single cluster", async () => {
+ it("shows progress only while provisioning is pending", async () => {
const user = userEvent.setup();
- let resolvePlacements:
- ((options: { hasMore: boolean; items: unknown[] }) => void) | undefined;
- findGatewayPlacementsMock.mockReturnValue(
+ let resolveProvision:
+ ((gateway: { id: string; name: string }) => void) | undefined;
+ provisionGateway.mockReturnValue(
new Promise((resolve) => {
- resolvePlacements = resolve;
+ resolveProvision = resolve;
}),
);
renderPage();
+ await enterNameAndSelectPublic(user);
+ const submit = screen.getByRole("button", { name: "Provision gateway" });
+ await user.click(submit);
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
+ await waitFor(() => {
+ expect((submit as HTMLButtonElement).disabled).toBe(true);
});
- await user.type(clusterInput, "E");
- await user.keyboard("{Backspace}");
- resolvePlacements?.({
- hasMore: false,
- items: [{ id: "cluster-east", name: "Cluster East", provider: "AWS" }],
+ expect(submit.classList.contains("pf-m-progress")).toBe(true);
+ resolveProvision?.({ id: "gateway-1", name: "team-gateway" });
+ await waitFor(() => {
+ expect(navigate).toHaveBeenCalledWith("/gateways/gateway-1");
});
-
- expect(await screen.findByText("Cluster East")).toBeTruthy();
- expect(clusterInput.value).toBe("");
});
- it("requires an explicit option after a free-form cluster search", async () => {
- const user = userEvent.setup();
- findGatewayPlacementsMock.mockResolvedValue({
- hasMore: false,
- items: [],
+ it("offers and defaults local-kind when it is the only available placement", async () => {
+ operations.getGatewayPlacementAvailability.mockResolvedValue({
+ awsPublic: false,
+ awsVpn: false,
+ ibmPublic: false,
+ ibmVpn: false,
+ localKind: true,
});
- renderPage();
-
- await user.type(
- screen.getByRole("combobox", { name: "Cluster" }),
- "Unknown placement",
- );
- expect(await screen.findByText("No matching clusters")).toBeTruthy();
- await user.type(
- screen.getByRole("textbox", { name: "Gateway name" }),
- "team-gateway",
- );
- await user.click(screen.getByRole("button", { name: "Provision gateway" }));
-
- expect(await screen.findByText("This field is required.")).toBeTruthy();
- expect(createGatewayMock).not.toHaveBeenCalled();
- });
-
- it("supports keyboard selection and bounded-result guidance", async () => {
const user = userEvent.setup();
- findGatewayPlacementsMock.mockResolvedValue({
- hasMore: true,
- items: [
- {
- id: "cluster-west",
- name: "Cluster West",
- provider: "GCP",
- region: "us-west1",
- status: "Ready",
- },
- ],
- });
renderPage();
expect(
- await screen.findByText(
- "More clusters are available. Refine your search to find a specific cluster.",
- ),
- ).toBeTruthy();
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
- });
- await user.type(clusterInput, "West");
- await screen.findByText("Cluster West");
- await user.keyboard("{ArrowDown}{Enter}");
-
- expect(clusterInput.value).toBe("Cluster West");
- });
-
- it("restores the last accepted placement when Escape cancels a search", async () => {
- const user = userEvent.setup();
- createGatewayMock.mockResolvedValue({
- ...createdGateway,
- clusterId: "cluster-east",
- });
- renderPage();
-
- const clusterInput = screen.getByRole("combobox", {
- name: "Cluster",
- });
- await user.type(clusterInput, "East");
- await user.click(await screen.findByText("Cluster East"));
-
- await user.clear(clusterInput);
- await user.type(clusterInput, "Uncommitted search");
- await user.keyboard("{Escape}");
-
- expect(clusterInput.value).toBe("Cluster East");
- expect(clusterInput.getAttribute("aria-expanded")).toBe("false");
-
+ (
+ await screen.findByRole("radio", {
+ name: "Use local-kind",
+ })
+ ).checked,
+ ).toBe(true);
+ expect(radio("Public").disabled).toBe(true);
+ expect(radio("VPN").disabled).toBe(true);
await user.type(
screen.getByRole("textbox", { name: "Gateway name" }),
"team-gateway",
);
await user.click(screen.getByRole("button", { name: "Provision gateway" }));
await waitFor(() => {
- expect(createGatewayMock).toHaveBeenCalledWith({
- clusterId: "cluster-east",
+ expect(provisionGateway).toHaveBeenCalledWith({
name: "team-gateway",
+ placement: { mode: "local-kind" },
});
});
});
- it("shows the available provider and region context for cluster options", async () => {
- const user = userEvent.setup();
- findGatewayPlacementsMock.mockResolvedValue({
- hasMore: false,
- items: [
- {
- id: "provider-only",
- name: "Provider cluster",
- provider: "AWS",
- },
- {
- id: "region-only",
- name: "Region cluster",
- provider: "",
- region: "eu-west-1",
- },
- {
- id: "without-context",
- name: "Unclassified cluster",
- provider: "",
- },
- ],
+ it("hides local-kind when managed placement is also available", () => {
+ operations.getGatewayPlacementAvailability.mockResolvedValue({
+ ...availablePlacement,
+ localKind: true,
});
renderPage();
- await user.click(screen.getByRole("combobox", { name: "Cluster" }));
- expect(await screen.findByText("Provider cluster")).toBeTruthy();
- expect(screen.getByText("Provider: AWS")).toBeTruthy();
- expect(screen.getByText("Region: eu-west-1")).toBeTruthy();
- expect(screen.getByText("Unclassified cluster")).toBeTruthy();
+ expect(screen.queryByRole("radio", { name: "Use local-kind" })).toBeNull();
+ expect(radio("Public").checked).toBe(false);
+ expect(radio("VPN").checked).toBe(false);
});
- it("retries a failed managed-cluster request", async () => {
- const user = userEvent.setup();
- findGatewayPlacementsMock
- .mockRejectedValueOnce(new Error("managed cluster API unavailable"))
- .mockResolvedValue({
- hasMore: false,
- items: [
- {
- id: "cluster-east",
- name: "Cluster East",
- provider: "AWS",
- region: "us-east-1",
- status: "Ready",
- },
- ],
- });
+ it("fails closed when placement availability cannot be loaded", async () => {
+ operations.getGatewayPlacementAvailability.mockRejectedValue(
+ new Error("unavailable"),
+ );
renderPage();
- await user.click(await screen.findByRole("button", { name: "Retry" }));
- await user.click(screen.getByRole("combobox", { name: "Cluster" }));
- expect(await screen.findByText("Cluster East")).toBeTruthy();
expect(
- screen.queryByText("Managed clusters could not be loaded"),
- ).toBeNull();
+ await screen.findByText(
+ "Placement availability could not be loaded. Cloud placement is disabled until availability can be verified.",
+ ),
+ ).toBeTruthy();
+ expect(radio("Public").disabled).toBe(true);
+ expect(radio("VPN").disabled).toBe(true);
});
- it("validates required values before sending a request", async () => {
- const user = userEvent.setup();
- findGatewayPlacementsMock.mockResolvedValue({
- hasMore: false,
- items: [
- { id: "cluster-east", name: "Cluster East", provider: "AWS" },
- { id: "cluster-west", name: "Cluster West", provider: "GCP" },
- ],
+ it("uses network-specific availability and explains the backend reason", async () => {
+ operations.getGatewayPlacementAvailability.mockResolvedValue({
+ awsPublic: true,
+ awsVpn: false,
+ awsReason: "no-eligible-cluster",
+ ibmPublic: false,
+ ibmVpn: false,
+ ibmReason: "no-eligible-cluster",
+ localKind: false,
});
+ const user = userEvent.setup();
renderPage();
-
- await user.click(screen.getByRole("button", { name: "Provision gateway" }));
-
- expect(await screen.findAllByText("This field is required.")).toHaveLength(
- 2,
+ const vpn = radio("VPN");
+ expect(vpn.disabled).toBe(true);
+ expect(vpn.getAttribute("aria-describedby")).toBe(
+ "placement-vpn-description",
);
- expect(createGatewayMock).not.toHaveBeenCalled();
- });
-
- it("shows progress only while the create request is pending", async () => {
- const user = userEvent.setup();
- let resolveRequest: ((gateway: typeof createdGateway) => void) | undefined;
- createGatewayMock.mockImplementation(
- () =>
- new Promise((resolve) => {
- resolveRequest = resolve;
- }),
+ expect(
+ (
+ await screen.findByText(
+ "VPN is unavailable: no eligible managed cluster is connected.",
+ )
+ ).id,
+ ).toBe("placement-vpn-description");
+ await user.click(screen.getByRole("radio", { name: "Public" }));
+
+ expect(radio("IBM Cloud").disabled).toBe(true);
+ expect(radio("IBM Cloud").getAttribute("aria-describedby")).toBe(
+ "provider-ibm-description",
);
- renderPage();
+ expect(
+ screen.getByText(
+ "IBM Cloud is unavailable: no eligible managed cluster is connected.",
+ ),
+ ).toBeTruthy();
+ });
- await user.type(
- screen.getByRole("textbox", { name: "Gateway name" }),
- "team-gateway",
- );
- const submitButton = screen.getByRole("button", {
- name: "Provision gateway",
+ it("explains when neither managed nor local placement is available", async () => {
+ operations.getGatewayPlacementAvailability.mockResolvedValue({
+ awsPublic: false,
+ awsVpn: false,
+ ibmPublic: false,
+ ibmVpn: false,
+ localKind: false,
});
- expect(submitButton.classList.contains("pf-m-progress")).toBe(false);
-
- await user.click(submitButton);
+ renderPage();
- await waitFor(() => {
- expect(submitButton.classList.contains("pf-m-progress")).toBe(true);
- });
- resolveRequest?.(createdGateway);
- await waitFor(() => {
- expect(navigateMock).toHaveBeenCalledWith("/gateways/gateway-1");
- });
+ expect(
+ await screen.findByText("No managed placement is currently available."),
+ ).toBeTruthy();
+ expect(screen.queryByRole("radio", { name: "Use local-kind" })).toBeNull();
});
});
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
index 52b1d88ad..7fc9c53af 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
@@ -2,20 +2,32 @@ import {
ActionGroup,
Alert,
Button,
+ Card,
+ CardBody,
+ CardHeader,
+ CardTitle,
Content,
Form,
FormGroup,
+ Gallery,
FormHelperText,
HelperText,
HelperTextItem,
PageSection,
+ Stack,
+ StackItem,
TextInput,
Title,
} from "@patternfly/react-core";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { useMemo } from "react";
-import { Controller, useForm, type Control } from "react-hook-form";
+import {
+ skipToken,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "@tanstack/react-query";
+import { useEffect, useMemo, type ReactNode } from "react";
+import { Controller, useForm, useWatch, type Control } from "react-hook-form";
import { FormattedMessage, useIntl } from "react-intl";
import { z } from "zod";
@@ -23,25 +35,140 @@ import { useGatewayUi } from "../gateway-ui-provider";
import type { GatewayProvisionInput } from "../application/gateway-types";
import { messages } from "../messages";
import { gatewayListQueryRoot, gatewayQueryKey } from "./gateway-data";
-import { GatewayPlacementSelect } from "./gateway-placement-select";
+import styles from "./gateway-create.module.css";
+import awsLogo from "../assets/aws-logo.svg";
+import ibmCloudLogo from "../assets/ibm-cloud.svg";
+
+const placementReasonMessages = {
+ "no-eligible-cluster": messages.noEligibleCluster,
+} as const;
export interface GatewayCreatePageProps {
onCreated?: (gatewayId: string) => Promise | void;
}
interface GatewayFormValues {
- clusterId: string | null;
name: string;
+ network: "public" | "vpn" | null;
+ provider: "aws" | "ibm" | null;
+ localKind: boolean;
}
interface GatewayTextFieldProps {
- control: Control;
+ control: Control;
fieldId: string;
isDisabled: boolean;
label: string;
name: "name";
}
+function Choice({
+ title,
+ description,
+ value,
+ selected,
+ icon,
+ isDisabled,
+ name,
+ onChoose,
+}: {
+ title: string;
+ description?: string;
+ value: string;
+ selected?: boolean;
+ icon?: string;
+ isDisabled?: boolean;
+ name: string;
+ onChoose: () => void;
+}) {
+ const labelId = `${name}-${value}-label`;
+ const descriptionId = description
+ ? `${name}-${value}-description`
+ : undefined;
+ return (
+ {
+ if (!isDisabled) onChoose();
+ }}
+ >
+ {
+ onChoose();
+ },
+ selectableActionAriaLabelledby: labelId,
+ selectableActionProps: {
+ value,
+ ...(descriptionId ? { "aria-describedby": descriptionId } : {}),
+ },
+ variant: "single",
+ }}
+ >
+
+
+ {icon ? (
+
+ ) : null}
+
+ {title}
+
+
+
+
+ {description ? (
+ {description}
+ ) : null}
+
+ );
+}
+
+function ChoiceGroup({
+ id,
+ label,
+ error,
+ children,
+}: {
+ id: string;
+ label: string;
+ error?: string;
+ children: ReactNode;
+}) {
+ const errorId = `${id}-error`;
+ return (
+
+
+
+
+ {children}
+
+
+
+ {error ? (
+
+
+
+ {error}
+
+
+
+ ) : null}
+
+ );
+}
+
function GatewayTextField({
control,
fieldId,
@@ -93,31 +220,44 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
const schema = useMemo(() => {
const requiredString = z.string().trim().min(1, requiredMessage);
- return z.object({
- clusterId: z
- .string({ error: requiredMessage })
- .nullable()
- .transform((value, context) => {
- if (value === null) {
- context.addIssue({ code: "custom", message: requiredMessage });
- return z.NEVER;
- }
- return value;
- }),
- name: requiredString,
- });
+ return z
+ .object({
+ name: requiredString,
+ network: z.enum(["public", "vpn"]).nullable(),
+ provider: z.enum(["aws", "ibm"]).nullable(),
+ localKind: z.boolean(),
+ })
+ .superRefine((values, context) => {
+ if (!values.localKind && values.network === null) {
+ context.addIssue({
+ code: "custom",
+ path: ["network"],
+ message: requiredMessage,
+ });
+ }
+ if (
+ !values.localKind &&
+ values.network !== null &&
+ values.provider === null
+ ) {
+ context.addIssue({
+ code: "custom",
+ path: ["provider"],
+ message: requiredMessage,
+ });
+ }
+ });
}, [requiredMessage]);
- const { control, handleSubmit } = useForm<
- GatewayFormValues,
- undefined,
- GatewayProvisionInput
- >({
- defaultValues: {
- clusterId: null,
- name: "",
- },
- resolver: zodResolver(schema),
- });
+ const { clearErrors, control, handleSubmit, setValue } =
+ useForm({
+ defaultValues: {
+ name: "",
+ network: null,
+ provider: null,
+ localKind: false,
+ },
+ resolver: zodResolver(schema) as never,
+ });
const createGateway = useMutation({
mutationFn: (values: GatewayProvisionInput) => {
@@ -134,11 +274,97 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
await navigation.navigate(navigation.detailHref(gateway.id));
}
},
+ onError: () => {
+ void queryClient.invalidateQueries({
+ queryKey: ["gateway", "placement-availability"],
+ });
+ },
});
+ const getPlacementAvailability =
+ gateways.getGatewayPlacementAvailability?.bind(gateways);
+ const availability = useQuery({
+ queryKey: ["gateway", "placement-availability"],
+ queryFn:
+ getPlacementAvailability === undefined
+ ? skipToken
+ : ({ signal }) => getPlacementAvailability(signal),
+ staleTime: 30_000,
+ });
+ const hasManagedPlacement = availability.data
+ ? availability.data.awsPublic ||
+ availability.data.awsVpn ||
+ availability.data.ibmPublic ||
+ availability.data.ibmVpn
+ : false;
+ const canUseLocalKind =
+ availability.data?.localKind === true && !hasManagedPlacement;
+ const defaultToLocalKind = canUseLocalKind && !hasManagedPlacement;
+ const publicPlacementAvailable = availability.data
+ ? availability.data.awsPublic || availability.data.ibmPublic
+ : false;
+ const vpnPlacementAvailable = availability.data?.awsVpn === true;
+
const submit = handleSubmit((values) => {
- createGateway.mutate(values);
+ if (values.localKind) {
+ createGateway.mutate({
+ name: values.name,
+ placement: { mode: "local-kind" },
+ });
+ return;
+ }
+ if (values.network === null || values.provider === null) return;
+ if (values.network === "vpn") {
+ if (values.provider !== "aws") return;
+ createGateway.mutate({
+ name: values.name,
+ placement: { network: "vpn", provider: "aws" },
+ });
+ return;
+ }
+ createGateway.mutate({
+ name: values.name,
+ placement: { network: "public", provider: values.provider },
+ });
});
+ const network = useWatch({ control, name: "network" });
+ const provider = useWatch({ control, name: "provider" });
+ const localKind = useWatch({ control, name: "localKind" });
+ useEffect(() => {
+ if (!availability.data || localKind) return;
+ if (network === "vpn") {
+ const availableProvider = availability.data.awsVpn ? "aws" : null;
+ if (provider === availableProvider) return;
+ setValue("provider", availableProvider, {
+ shouldValidate: true,
+ });
+ } else if (
+ network === "public" &&
+ (provider === null ||
+ (provider === "ibm" && !availability.data.ibmPublic) ||
+ (provider === "aws" && !availability.data.awsPublic))
+ ) {
+ setValue(
+ "provider",
+ availability.data.ibmPublic
+ ? "ibm"
+ : availability.data.awsPublic
+ ? "aws"
+ : null,
+ { shouldValidate: true },
+ );
+ }
+ }, [availability.data, localKind, network, provider, setValue]);
+ useEffect(() => {
+ if (defaultToLocalKind && !localKind) {
+ setValue("localKind", true, { shouldValidate: true });
+ setValue("network", null);
+ setValue("provider", null);
+ clearErrors(["network", "provider"]);
+ } else if (!canUseLocalKind && localKind) {
+ setValue("localKind", false, { shouldValidate: true });
+ }
+ }, [canUseLocalKind, clearErrors, defaultToLocalKind, localKind, setValue]);
return (
<>
@@ -174,18 +400,202 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
label={intl.formatMessage(messages.gatewayName)}
name="name"
/>
+ {canUseLocalKind ? (
+ (
+
+ {
+ field.onChange(true);
+ setValue("network", null);
+ setValue("provider", null);
+ clearErrors(["network", "provider"]);
+ }}
+ />
+
+ )}
+ />
+ ) : null}
(
-
+ label={intl.formatMessage(messages.networkAccess)}
+ >
+ {
+ setValue("localKind", false);
+ field.onChange("public");
+ setValue(
+ "provider",
+ availability.data?.ibmPublic
+ ? "ibm"
+ : availability.data?.awsPublic
+ ? "aws"
+ : null,
+ );
+ }}
+ />
+ {
+ setValue("localKind", false);
+ field.onChange("vpn");
+ setValue(
+ "provider",
+ availability.data?.awsVpn === true ? "aws" : null,
+ );
+ }}
+ />
+
)}
/>
+ {network !== null ? (
+ (
+
+ {
+ field.onChange("aws");
+ }}
+ />
+ {
+ field.onChange("ibm");
+ }}
+ />
+
+ )}
+ />
+ ) : null}
+ {availability.data && !hasManagedPlacement && !canUseLocalKind ? (
+
+ ) : null}
+ {availability.isError ? (
+
+ ) : null}
+
+
+
void;
- value: string | null;
-}
-
-function placementDescription(
- placement: GatewayPlacement,
- formatMessage: ReturnType["formatMessage"],
-) {
- const provider = placement.provider.trim();
- const region = placement.region?.trim() ?? "";
- if (provider && region) {
- return formatMessage(messages.clusterProviderAndRegion, {
- provider,
- region,
- });
- }
- if (provider) {
- return formatMessage(messages.clusterProvider, { provider });
- }
- if (region) {
- return formatMessage(messages.clusterRegion, { region });
- }
- return undefined;
-}
-
-export function GatewayPlacementSelect({
- error,
- isDisabled,
- onChange,
- value,
-}: GatewayPlacementSelectProps) {
- const intl = useIntl();
- const { gateways } = useGatewayUi();
- const [isOpen, setIsOpen] = useState(false);
- const [inputValue, setInputValue] = useState("");
- const [searchValue, setSearchValue] = useState("");
- const [focusedItemIndex, setFocusedItemIndex] = useState(null);
- const [activeItemId, setActiveItemId] = useState();
- const inputRef = useRef(null);
- const lastAcceptedSelectionRef = useRef<{
- label: string;
- value: string | null;
- }>({ label: "", value: null });
- // Once the user touches the field, the single-cluster preselection below must
- // never override what they did (clearing, typing, choosing).
- const [userInteracted, setUserInteracted] = useState(false);
- const normalizedSearch = searchValue.trim();
- const debouncedSearch = useDebouncedValue(
- normalizedSearch,
- gatewaySearchDebounceMilliseconds,
- );
- const isSearchPending = normalizedSearch !== debouncedSearch;
- const placementQuery = useQuery({
- queryFn: ({ signal }) =>
- gateways.findGatewayPlacements(debouncedSearch, signal),
- queryKey: gatewayPlacementQueryKey(debouncedSearch),
- staleTime: gatewayPlacementStaleMilliseconds,
- });
- const options: PlacementOption[] = [
- ...(!isSearchPending
- ? (placementQuery.data?.items.map((placement) => ({
- description: placementDescription(placement, intl.formatMessage),
- key: `cluster:${placement.id}`,
- label: placement.name,
- value: placement.id,
- })) ?? [])
- : []),
- ];
-
- if (isSearchPending || placementQuery.isPending) {
- options.push({
- isDisabled: true,
- key: "loading",
- label: intl.formatMessage(messages.loadingClusters),
- value: loadingValue,
- });
- } else if (options.length === 0) {
- options.push({
- isDisabled: true,
- key: "no-results",
- label: intl.formatMessage(messages.noMatchingClusters),
- value: noResultsValue,
- });
- }
-
- // Only registered clusters are offered (the adapter filters them), and a
- // gateway must name one. When exactly one exists and the user has not
- // touched the field, preselect it so the common single-cluster case needs
- // no extra click; with several clusters the choice stays explicit.
- const unfilteredResult =
- normalizedSearch === "" && !isSearchPending
- ? placementQuery.data
- : undefined;
- const autoSelection =
- value === null &&
- !userInteracted &&
- unfilteredResult &&
- !unfilteredResult.hasMore &&
- unfilteredResult.items.length === 1
- ? unfilteredResult.items[0]
- : undefined;
- useEffect(() => {
- if (!autoSelection) {
- return;
- }
- lastAcceptedSelectionRef.current = {
- label: autoSelection.name,
- value: autoSelection.id,
- };
- onChange(autoSelection.id);
- }, [autoSelection, onChange]);
- // The text box shows what the user typed or chose; when nothing was typed
- // and a cluster is selected (the preselection case), it shows that
- // cluster's name, resolved from the same unfiltered result.
- const displayedInputValue =
- inputValue === "" && normalizedSearch === "" && value !== null
- ? (unfilteredResult?.items.find(({ id }) => id === value)?.name ?? "")
- : inputValue;
-
- const resetFocus = () => {
- setFocusedItemIndex(null);
- setActiveItemId(undefined);
- };
- const closeMenu = () => {
- setIsOpen(false);
- resetFocus();
- };
- const chooseOption = (option: PlacementOption) => {
- if (typeof option.value !== "string" || option.isDisabled) {
- return;
- }
- setUserInteracted(true);
- lastAcceptedSelectionRef.current = {
- label: option.label,
- value: option.value,
- };
- onChange(option.value);
- setInputValue(option.label);
- setSearchValue("");
- closeMenu();
- };
- const focusOption = (index: number) => {
- setFocusedItemIndex(index);
- setActiveItemId(`gateway-cluster-option-${String(index)}`);
- };
- const moveFocus = (direction: -1 | 1) => {
- if (!isOpen) {
- setIsOpen(true);
- }
- if (options.every(({ isDisabled: optionDisabled }) => optionDisabled)) {
- return;
- }
-
- let nextIndex = focusedItemIndex ?? (direction === 1 ? -1 : 0);
- do {
- nextIndex = (nextIndex + direction + options.length) % options.length;
- } while (options[nextIndex]?.isDisabled);
- focusOption(nextIndex);
- };
- const toggle = (toggleRef: React.Ref) => (
- {
- setIsOpen((open) => !open);
- inputRef.current?.focus();
- }}
- ref={toggleRef}
- status={error ? "danger" : undefined}
- variant="typeahead"
- >
-
- {
- setUserInteracted(true);
- setInputValue(nextValue);
- setSearchValue(nextValue);
- onChange(null);
- resetFocus();
- setIsOpen(true);
- }}
- onClick={() => {
- setIsOpen(true);
- }}
- onKeyDown={(event) => {
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
- event.preventDefault();
- moveFocus(event.key === "ArrowDown" ? 1 : -1);
- } else if (event.key === "Enter") {
- if (isOpen && focusedItemIndex !== null) {
- event.preventDefault();
- const focusedOption = options[focusedItemIndex];
- if (focusedOption) {
- chooseOption(focusedOption);
- }
- } else if (!isOpen) {
- event.preventDefault();
- setIsOpen(true);
- }
- } else if (event.key === "Escape" && isOpen) {
- event.preventDefault();
- const lastAcceptedSelection = lastAcceptedSelectionRef.current;
- setInputValue(lastAcceptedSelection.label);
- setSearchValue("");
- onChange(lastAcceptedSelection.value);
- closeMenu();
- }
- }}
- placeholder={intl.formatMessage(messages.selectCluster)}
- role="combobox"
- value={displayedInputValue}
- />
-
- }
- onClick={() => {
- setUserInteracted(true);
- setInputValue("");
- setSearchValue("");
- onChange(null);
- resetFocus();
- inputRef.current?.focus();
- }}
- variant="plain"
- />
-
-
-
- );
-
- return (
-
- {
- if (!open) {
- closeMenu();
- }
- }}
- onSelect={(_event, selectedValue) => {
- const option = options.find(
- ({ value: optionValue }) => optionValue === selectedValue,
- );
- if (option) {
- chooseOption(option);
- }
- }}
- selected={value ?? undefined}
- toggle={toggle}
- variant="typeahead"
- >
-
- {options.map((option, index) => (
-
- {option.label}
-
- ))}
-
-
- {error ? (
-
-
-
- {error}
-
-
-
- ) : (
-
-
-
- {intl.formatMessage(messages.registeredClustersOnly)}
-
-
-
- )}
- {placementQuery.data?.hasMore &&
- !isSearchPending &&
- !placementQuery.isFetching ? (
-
-
-
- {intl.formatMessage(messages.moreClustersAvailable)}
-
-
-
- ) : null}
- {placementQuery.isError && !isSearchPending ? (
- void placementQuery.refetch()}>
- {intl.formatMessage(messages.retry)}
-
- }
- isInline
- isLiveRegion
- title={intl.formatMessage(messages.clusterLoadError)}
- variant="warning"
- >
- {intl.formatMessage(messages.clusterLoadErrorBody)}
-
- ) : null}
-
- );
-}
diff --git a/packages/gateway-management-ui/src/index.ts b/packages/gateway-management-ui/src/index.ts
index 2dd254689..cb4469d5e 100644
--- a/packages/gateway-management-ui/src/index.ts
+++ b/packages/gateway-management-ui/src/index.ts
@@ -13,6 +13,7 @@ export type {
GatewayOperations,
GatewayPage as GatewayPageResult,
GatewayPlacement,
+ GatewayPlacementAvailability,
GatewayPlacementOptions,
GatewayProvisionInput,
ProvisioningCondition,
diff --git a/packages/gateway-management-ui/src/messages.ts b/packages/gateway-management-ui/src/messages.ts
index 9094778f0..a27328cde 100644
--- a/packages/gateway-management-ui/src/messages.ts
+++ b/packages/gateway-management-ui/src/messages.ts
@@ -506,6 +506,107 @@ export const messages = defineMessages({
defaultMessage: "Configure a new OpenShell gateway.",
description: "Supporting text on the gateway provisioning page.",
},
+ /* eslint-disable sort-keys -- Keep gateway placement descriptors together for localization review. */
+ networkAccess: {
+ id: "app.page.gatewayProvision.networkAccess",
+ defaultMessage: "Network access",
+ description: "Network exposure mode for a gateway.",
+ },
+ cloudProvider: {
+ id: "app.page.gatewayProvision.cloudProvider",
+ defaultMessage: "Cloud provider",
+ description: "Cloud provider for public gateway placement.",
+ },
+ publicNetwork: {
+ id: "app.page.gatewayProvision.publicNetwork",
+ defaultMessage: "Public",
+ description: "Public network placement option.",
+ },
+ publicNetworkDescription: {
+ id: "app.page.gatewayProvision.publicNetworkDescription",
+ defaultMessage:
+ "Accessible through a public endpoint. Choose a cloud provider below.",
+ description: "Description of public network placement.",
+ },
+ vpnNetwork: {
+ id: "app.page.gatewayProvision.vpnNetwork",
+ defaultMessage: "VPN",
+ description: "VPN network placement option.",
+ },
+ vpnNetworkDescription: {
+ id: "app.page.gatewayProvision.vpnNetworkDescription",
+ defaultMessage:
+ "Private network access. AWS is required for VPN placement.",
+ description: "Description of VPN placement.",
+ },
+ awsProvider: {
+ id: "app.page.gatewayProvision.awsProvider",
+ defaultMessage: "AWS",
+ description: "AWS cloud provider option.",
+ },
+ ibmCloudProvider: {
+ id: "app.page.gatewayProvision.ibmCloudProvider",
+ defaultMessage: "IBM Cloud",
+ description: "IBM Cloud provider option.",
+ },
+ unavailableProvider: {
+ id: "app.page.gatewayProvision.unavailableProvider",
+ defaultMessage: "{provider} is unavailable: {reason}",
+ description:
+ "Explanation shown when a provider cannot currently accept placement.",
+ },
+ unavailableNetwork: {
+ id: "app.page.gatewayProvision.unavailableNetwork",
+ defaultMessage: "{network} is unavailable: {reason}",
+ description:
+ "Explanation shown when a network cannot currently accept placement.",
+ },
+ noEligibleCluster: {
+ id: "app.page.gatewayProvision.noEligibleCluster",
+ defaultMessage: "no eligible managed cluster is connected.",
+ description:
+ "Provider unavailability reason when no matching connected cluster exists.",
+ },
+ noManagedPlacement: {
+ id: "app.page.gatewayProvision.noManagedPlacement",
+ defaultMessage: "No managed placement is currently available.",
+ description:
+ "Warning shown when no provider or local development placement can be selected.",
+ },
+ vpnRequiresAws: {
+ id: "app.page.gatewayProvision.vpnRequiresAws",
+ defaultMessage: "VPN placement requires AWS.",
+ description: "Explanation shown when IBM Cloud is disabled for VPN.",
+ },
+ localKindPlacement: {
+ id: "app.page.gatewayProvision.localKindPlacement",
+ defaultMessage: "Use local-kind",
+ description: "Local Kind placement option.",
+ },
+ localKindPlacementDescription: {
+ id: "app.page.gatewayProvision.localKindPlacementDescription",
+ defaultMessage: "Use the local Kind cluster for development.",
+ description: "Description of local Kind placement.",
+ },
+ localDevelopment: {
+ id: "app.page.gatewayProvision.localDevelopment",
+ defaultMessage: "Local development",
+ description: "Form group label for local Kind placement.",
+ },
+ placementHelp: {
+ id: "app.page.gatewayProvision.placementHelp",
+ defaultMessage:
+ "A matching managed cluster is selected at random for the chosen network and provider.",
+ description: "Explanation of automatic gateway placement.",
+ },
+ placementAvailabilityError: {
+ id: "app.page.gatewayProvision.placementAvailabilityError",
+ defaultMessage:
+ "Placement availability could not be loaded. Cloud placement is disabled until availability can be verified.",
+ description:
+ "Warning shown when current gateway placement availability cannot be retrieved.",
+ },
+ /* eslint-enable sort-keys */
provisioningGateway: {
id: "app.page.gatewayProvision.pending",
defaultMessage: "Provisioning gateway",
diff --git a/scripts/sdk-generator/model.go b/scripts/sdk-generator/model.go
index cc291e505..8d945a928 100644
--- a/scripts/sdk-generator/model.go
+++ b/scripts/sdk-generator/model.go
@@ -26,6 +26,7 @@ type Resource struct {
Models []Model
ListParameters []Field
Fields []Field
+ CreateFields []Field
RequiredFields []string
PatchFields []Field
StatusPatchFields []Field
diff --git a/scripts/sdk-generator/parser.go b/scripts/sdk-generator/parser.go
index 71ebca92b..ef7860747 100644
--- a/scripts/sdk-generator/parser.go
+++ b/scripts/sdk-generator/parser.go
@@ -111,6 +111,8 @@ func projectScopedResource(document *ir.Document, collection *ir.ResourceView, a
})
}
+ collectionRelative := relativeAPIPath(collection.Path, apiPrefix)
+ itemRelative := relativeAPIPath(itemPath, apiPrefix)
return Resource{
Name: name,
Plural: resourcePlural(name),
@@ -118,10 +120,10 @@ func projectScopedResource(document *ir.Document, collection *ir.ResourceView, a
Scoped: true,
ScopeParameters: scopeParameters,
ItemParameter: &itemParameter,
- GoCollectionPath: goPathExpression(collection.Path, scopeParameters, nil),
- GoItemPath: goPathExpression(itemPath, scopeParameters, &itemParameter),
- TSCollectionPath: tsPathExpression(collection.Path, scopeParameters, nil),
- TSItemPath: tsPathExpression(itemPath, scopeParameters, &itemParameter),
+ GoCollectionPath: goPathExpression(collectionRelative, scopeParameters, nil),
+ GoItemPath: goPathExpression(itemRelative, scopeParameters, &itemParameter),
+ TSCollectionPath: tsPathExpression(collectionRelative, scopeParameters, nil),
+ TSItemPath: tsPathExpression(itemRelative, scopeParameters, &itemParameter),
ListType: listType,
ItemType: itemType,
CreateRequestType: createRequestType,
@@ -333,10 +335,13 @@ func projectedTypes(document *ir.Document, reference *ir.SchemaReference, requir
return "", "", "any", "unknown", ""
}
openAPIType, format := schemaType(schema)
+ if strings.HasSuffix(reference.Ref, "GatewayPlacementIntent") {
+ return openAPIType, format, "GatewayPlacementIntent", "GatewayPlacementIntent", "GatewayPlacementIntent"
+ }
modelRef := ""
goType := ""
tsType := ""
- if schema.Name != "" && (len(schema.Enum) > 0 || len(schema.Properties) > 0 || len(schema.AllOf) > 0) {
+ if schema.Name != "" && (len(schema.Enum) > 0 || len(document.EffectiveProperties(reference.Ref)) > 0 || len(schema.AllOf) > 0) {
modelRef = schema.Name
goType = schema.Name
tsType = schema.Name
@@ -424,13 +429,35 @@ func tsPathExpression(path string, scope []PathParameter, item *PathParameter) s
func projectResource(document *ir.Document, schema *ir.Schema, collection *ir.ResourceView) (Resource, error) {
fields, required := projectFields(document, schema.Ref, true)
+ createFields, _ := fieldsForCreate(document, schema.Name, collection.Path)
+ var roots []string
+ if createSchema := createRequestSchema(document, collection.Path); createSchema != "" {
+ roots = append(roots, createSchema)
+ }
+ if schema.Name == "Gateway" && document.Schema("GatewayPlacementAvailability") != nil {
+ roots = append(roots, "GatewayPlacementAvailability")
+ }
+ models := projectModels(document, roots)
+ createSchemaName := ""
+ if createSchema := createRequestSchema(document, collection.Path); createSchema != "" {
+ if createSchemaModel := document.Schema(createSchema); createSchemaModel != nil {
+ createSchemaName = createSchemaModel.Name
+ }
+ }
+ filtered := models[:0]
+ for _, model := range models {
+ if model.Name != schema.Name && model.Name != createSchemaName {
+ filtered = append(filtered, model)
+ }
+ }
+ models = filtered
patchFields, _ := projectFieldsByName(document, schema.Name+"PatchRequest", false)
statusPatchFields, _ := projectFieldsByName(document, schema.Name+"StatusPatchRequest", false)
resource := Resource{
Name: schema.Name, Plural: resourcePlural(schema.Name), PathSegment: lastLiteralSegment(collection.Path),
AbsoluteCollectionPath: collection.Path,
- Fields: fields, RequiredFields: required, PatchFields: patchFields,
+ Fields: fields, RequiredFields: required, CreateFields: createFields, Models: models, PatchFields: patchFields,
StatusPatchFields: statusPatchFields, HasStatusPatch: len(statusPatchFields) > 0,
}
for _, view := range document.ResourceViews {
@@ -456,6 +483,76 @@ func projectResource(document *ir.Document, schema *ir.Schema, collection *ir.Re
return resource, nil
}
+func fieldsForCreate(document *ir.Document, resourceName, path string) ([]Field, []string) {
+ if resourceName != "Gateway" {
+ return writableResourceFields(document, resourceName)
+ }
+ ref := createRequestSchema(document, path)
+ if ref == "" {
+ return writableResourceFields(document, resourceName)
+ }
+ fields, required := projectFields(document, ref, false)
+ // cluster_id remains accepted by the server only as a deprecated
+ // compatibility path for legacy internal callers. Generated create clients
+ // must not expose it as a placement decision; placement is represented by
+ // the intent field instead.
+ filtered := fields[:0]
+ for _, field := range fields {
+ if field.Name != "cluster_id" {
+ filtered = append(filtered, field)
+ }
+ }
+ fields = filtered
+ filteredRequired := required[:0]
+ for _, name := range required {
+ if name != "cluster_id" {
+ filteredRequired = append(filteredRequired, name)
+ }
+ }
+ required = filteredRequired
+ for index := range fields {
+ if fields[index].Name == "name" || fields[index].Name == "placement" {
+ fields[index].Required = true
+ }
+ if fields[index].Name == "placement" {
+ fields[index].GoType = "GatewayPlacementIntent"
+ fields[index].TSType = "GatewayPlacementIntent"
+ fields[index].ModelRef = "GatewayPlacementIntent"
+ }
+ }
+ return fields, required
+}
+
+func writableResourceFields(document *ir.Document, resourceName string) ([]Field, []string) {
+ fields, required := projectFieldsByName(document, resourceName, true)
+ writable := make([]Field, 0, len(fields))
+ writableRequired := make([]string, 0, len(required))
+ for _, field := range fields {
+ if field.ReadOnly {
+ continue
+ }
+ writable = append(writable, field)
+ if field.Required {
+ writableRequired = append(writableRequired, field.Name)
+ }
+ }
+ return writable, writableRequired
+}
+
+func createRequestSchema(document *ir.Document, path string) string {
+ operation := operationAt(document, path, "POST")
+ if operation == nil || operation.RequestBody == nil {
+ return ""
+ }
+ for _, content := range operation.RequestBody.Content {
+ if content.Schema == nil || content.Schema.Ref == "" {
+ continue
+ }
+ return content.Schema.Ref
+ }
+ return ""
+}
+
func projectFieldsByName(document *ir.Document, name string, includeReadOnly bool) ([]Field, []string) {
for _, schema := range document.Schemas {
if schema.Name == name {
diff --git a/scripts/sdk-generator/parser_test.go b/scripts/sdk-generator/parser_test.go
index 80ac38ecc..52770a5dc 100644
--- a/scripts/sdk-generator/parser_test.go
+++ b/scripts/sdk-generator/parser_test.go
@@ -1,6 +1,7 @@
package main
import (
+ "os"
"path/filepath"
"strings"
"testing"
@@ -8,7 +9,7 @@ import (
func TestParseSpecProjectsScopedServiceAccountResource(t *testing.T) {
specPath := filepath.Join("..", "..", "components", "api-server", "openapi", "openapi.yaml")
- spec, err := parseSpec(specPath, "/api/hypershell/v1")
+ spec, err := parseSpec(specPath, "/api/hypershell")
if err != nil {
t.Fatalf("parse spec: %v", err)
}
@@ -69,3 +70,90 @@ func TestParseSpecProjectsScopedServiceAccountResource(t *testing.T) {
}
}
}
+
+func TestParseSpecProjectsGatewayCreateRequestFromOperationSchema(t *testing.T) {
+ specPath := filepath.Join("..", "..", "components", "api-server", "openapi", "openapi.yaml")
+ spec, err := parseSpec(specPath, "/api/hypershell")
+ if err != nil {
+ t.Fatalf("parse spec: %v", err)
+ }
+ for _, resource := range spec.Resources {
+ if resource.Name != "Gateway" {
+ continue
+ }
+ if len(resource.CreateFields) == 0 {
+ t.Fatal("gateway create fields are missing")
+ }
+ fields := make(map[string]Field, len(resource.CreateFields))
+ for _, field := range resource.CreateFields {
+ fields[field.Name] = field
+ }
+ if fields["cluster_id"].Name != "" {
+ t.Fatal("cluster_id must not be in the create request")
+ }
+ if !fields["name"].Required || !fields["placement"].Required {
+ t.Fatal("name and placement must be required")
+ }
+ if fields["placement"].TSType != "GatewayPlacementIntent" {
+ t.Fatalf("placement type = %q", fields["placement"].TSType)
+ }
+ return
+ }
+ t.Fatal("gateway resource is missing")
+}
+
+func TestGeneratedGatewayBuildersValidateRequiredPlacement(t *testing.T) {
+ specPath := filepath.Join("..", "..", "components", "api-server", "openapi", "openapi.yaml")
+ spec, err := parseSpec(specPath, "/api/hypershell")
+ if err != nil {
+ t.Fatalf("parse spec: %v", err)
+ }
+
+ outDir := t.TempDir()
+ header := GeneratedHeader{SpecPath: specPath, SpecHash: "test"}
+ goOut := filepath.Join(outDir, "go")
+ if err := generateGo(spec, goOut, header); err != nil {
+ t.Fatalf("generate Go SDK: %v", err)
+ }
+ goGateway, err := os.ReadFile(filepath.Join(goOut, "types", "gateway.go"))
+ if err != nil {
+ t.Fatalf("read generated Go gateway: %v", err)
+ }
+ if !strings.Contains(string(goGateway), "if !b.placementSet") {
+ t.Fatal("generated Go gateway builder does not validate required placement")
+ }
+
+ tsOut := filepath.Join(outDir, "typescript")
+ if err := generateTypeScript(spec, tsOut, header); err != nil {
+ t.Fatalf("generate TypeScript SDK: %v", err)
+ }
+ tsGateway, err := os.ReadFile(filepath.Join(tsOut, "src", "gateway.ts"))
+ if err != nil {
+ t.Fatalf("read generated TypeScript gateway: %v", err)
+ }
+ if !strings.Contains(string(tsGateway), "this.data['placement'] === undefined") {
+ t.Fatal("generated TypeScript gateway builder does not validate required placement")
+ }
+}
+
+func TestParseSpecPreservesWritableCreateFieldsWithoutPostOperation(t *testing.T) {
+ specPath := filepath.Join("..", "..", "components", "api-server", "openapi", "openapi.yaml")
+ spec, err := parseSpec(specPath, "/api/hypershell")
+ if err != nil {
+ t.Fatalf("parse spec: %v", err)
+ }
+ for _, resource := range spec.Resources {
+ if resource.Name != "Role" {
+ continue
+ }
+ fields := make(map[string]Field, len(resource.CreateFields))
+ for _, field := range resource.CreateFields {
+ fields[field.Name] = field
+ }
+ if !fields["name"].Required || fields["description"].Name == "" {
+ t.Fatalf("role create fields were not preserved: %#v", fields)
+ }
+ return
+ }
+ t.Fatal("role resource is missing")
+}
diff --git a/scripts/sdk-generator/templates/go/client.go.tmpl b/scripts/sdk-generator/templates/go/client.go.tmpl
index 375ab6545..ccc82e70b 100644
--- a/scripts/sdk-generator/templates/go/client.go.tmpl
+++ b/scripts/sdk-generator/templates/go/client.go.tmpl
@@ -22,7 +22,15 @@ type {{.Resource.Name}}API struct {
func (c *Client) {{.Resource.Plural}}() *{{.Resource.Name}}API {
return &{{.Resource.Name}}API{client: c}
}
-
+{{if eq .Resource.Name "Gateway"}}
+func (a *GatewayAPI) PlacementAvailability(ctx context.Context) (*types.GatewayPlacementAvailability, error) {
+ var result types.GatewayPlacementAvailability
+ if err := a.client.do(ctx, http.MethodGet, "/gateways/placement-availability", nil, http.StatusOK, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+{{end}}
func (a *{{.Resource.Name}}API) Create(ctx context.Context, resource *types.{{.Resource.Name}}) (*types.{{.Resource.Name}}, error) {
body, err := json.Marshal(resource)
if err != nil {
diff --git a/scripts/sdk-generator/templates/go/types.go.tmpl b/scripts/sdk-generator/templates/go/types.go.tmpl
index 06f96f605..8f1005fdd 100644
--- a/scripts/sdk-generator/templates/go/types.go.tmpl
+++ b/scripts/sdk-generator/templates/go/types.go.tmpl
@@ -13,11 +13,28 @@ import (
{{- end}}
)
+{{- range .Resource.Models}}{{$model := .}}
+{{- if ne .Name "GatewayCreateRequest"}}
+{{- if .IsEnum}}
+type {{.Name}} string
+{{else}}
+type {{.Name}} struct {
+{{- range .Fields}}
+ {{.GoName}} {{.GoType}} {{.JSONTag}}
+{{- end}}
+}
+{{end}}
+{{end}}
+{{end}}
+
type {{.Resource.Name}} struct {
ObjectReference
{{range .Resource.Fields}}
{{.GoName}} {{.GoType}} {{.JSONTag}}
{{- end}}
+{{if eq .Resource.Name "Gateway"}}
+ Placement GatewayPlacementIntent `json:"placement,omitempty"`
+{{end}}
}
type {{.Resource.Name}}List struct {
@@ -33,26 +50,38 @@ func (l *{{.Resource.Name}}List) GetSize() int { return l.Size
type {{.Resource.Name}}Builder struct {
resource {{.Resource.Name}}
errors []error
+{{- range .Resource.CreateFields}}
+ {{- if and .Required (ne .GoType "string")}}
+ {{.GoName | lowerFirst}}Set bool
+ {{- end}}
+{{- end}}
}
func New{{.Resource.Name}}Builder() *{{.Resource.Name}}Builder {
return &{{.Resource.Name}}Builder{}
}
-{{range .Resource.Fields}}{{if isWritable .}}
+{{range .Resource.CreateFields}}
func (b *{{$.Resource.Name}}Builder) {{.GoName}}(v {{.GoType}}) *{{$.Resource.Name}}Builder {
b.resource.{{.GoName}} = v
+ {{- if and .Required (ne .GoType "string")}}
+ b.{{.GoName | lowerFirst}}Set = true
+ {{- end}}
return b
}
-{{end}}{{end}}
+{{end}}
func (b *{{.Resource.Name}}Builder) Build() (*{{.Resource.Name}}, error) {
-{{- range .Resource.Fields}}
+{{- range .Resource.CreateFields}}
{{- if .Required}}
{{- if eq .GoType "string"}}
if b.resource.{{.GoName}} == "" {
b.errors = append(b.errors, fmt.Errorf("{{.Name}} is required"))
}
+ {{- else}}
+ if !b.{{.GoName | lowerFirst}}Set {
+ b.errors = append(b.errors, fmt.Errorf("{{.Name}} is required"))
+ }
{{- end}}
{{- end}}
{{- end}}
diff --git a/scripts/sdk-generator/templates/ts/client.ts.tmpl b/scripts/sdk-generator/templates/ts/client.ts.tmpl
index e0fef22f4..d8d5eb738 100644
--- a/scripts/sdk-generator/templates/ts/client.ts.tmpl
+++ b/scripts/sdk-generator/templates/ts/client.ts.tmpl
@@ -5,11 +5,15 @@
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
-import type { {{.Resource.Name}}, {{.Resource.Name}}List, {{.Resource.Name}}CreateRequest{{if .Resource.HasPatch}}, {{.Resource.Name}}PatchRequest{{end}}{{if .Resource.HasStatusPatch}}, {{.Resource.Name}}StatusPatchRequest{{end}} } from './{{.Resource.Name | snakeCase}}.js';
+import type { {{.Resource.Name}}, {{.Resource.Name}}List, {{.Resource.Name}}CreateRequest{{if .Resource.HasPatch}}, {{.Resource.Name}}PatchRequest{{end}}{{if .Resource.HasStatusPatch}}, {{.Resource.Name}}StatusPatchRequest{{end}}{{if eq .Resource.Name "Gateway"}}, GatewayPlacementAvailability{{end}} } from './{{.Resource.Name | snakeCase}}.js';
export class {{.Resource.Name}}API {
constructor(private readonly config: SDKClientConfig) {}
-
+{{if eq .Resource.Name "Gateway"}}
+ async placementAvailability(opts?: RequestOptions): Promise {
+ return sdkFetch(this.config, 'GET', '/gateways/placement-availability', undefined, opts);
+ }
+{{end}}
async create(data: {{.Resource.Name}}CreateRequest, opts?: RequestOptions): Promise<{{.Resource.Name}}> {
return sdkFetch<{{.Resource.Name}}>(this.config, 'POST', '{{.Resource.AbsoluteCollectionPath}}', data, opts);
}
diff --git a/scripts/sdk-generator/templates/ts/types.ts.tmpl b/scripts/sdk-generator/templates/ts/types.ts.tmpl
index da06564df..1e57b1e82 100644
--- a/scripts/sdk-generator/templates/ts/types.ts.tmpl
+++ b/scripts/sdk-generator/templates/ts/types.ts.tmpl
@@ -5,6 +5,25 @@
import type { ObjectReference, ListMeta } from './base.js';
+{{- range .Resource.Models}}
+{{- if ne .Name "GatewayCreateRequest"}}
+{{- if eq .Name "GatewayPlacementIntent"}}
+export type GatewayPlacementIntent =
+ | { mode: 'local-kind' }
+ | { network: 'public'; provider: 'aws' | 'ibm' }
+ | { network: 'vpn'; provider: 'aws' };
+{{- else if .IsEnum}}
+export type {{.Name}} = {{range $index, $value := .EnumValues}}{{if $index}} | {{end}}'{{$value.Value}}'{{end}};
+{{else}}
+export type {{.Name}} = {
+{{- range .Fields}}
+ {{.Name}}{{if not .Required}}?{{end}}: {{.TSType}};
+{{- end}}
+};
+{{end}}
+{{end}}
+{{end}}
+
export type {{.Resource.Name}} = ObjectReference & {
{{- range .Resource.Fields}}
{{.Name}}: {{.TSType}};
@@ -16,9 +35,9 @@ export type {{.Resource.Name}}List = ListMeta & {
};
export type {{.Resource.Name}}CreateRequest = {
-{{- range .Resource.Fields}}{{if isWritable .}}
+{{- range .Resource.CreateFields}}
{{.Name}}{{if not .Required}}?{{end}}: {{.TSType}};
-{{- end}}{{end}}
+{{- end}}
};
export type {{.Resource.Name}}PatchRequest = {
@@ -36,19 +55,23 @@ export type {{.Resource.Name}}StatusPatchRequest = {
export class {{.Resource.Name}}Builder {
private data: Record = {};
-{{range .Resource.Fields}}{{if isWritable .}}
+{{range .Resource.CreateFields}}
{{.TSName}}(value: {{.TSType}}): this {
this.data['{{.Name}}'] = value;
return this;
}
-{{end}}{{end}}
+{{end}}
build(): {{.Resource.Name}}CreateRequest {
-{{- range .Resource.Fields}}
+{{- range .Resource.CreateFields}}
{{- if .Required}}
{{- if eq .TSType "string"}}
if (!this.data['{{.Name}}']) {
throw new Error('{{.Name}} is required');
}
+{{- else}}
+ if (this.data['{{.Name}}'] === undefined) {
+ throw new Error('{{.Name}} is required');
+ }
{{- end}}
{{- end}}
{{- end}}
diff --git a/skills/RECONCILE.md b/skills/RECONCILE.md
index 76e93c1a1..bf11260db 100644
--- a/skills/RECONCILE.md
+++ b/skills/RECONCILE.md
@@ -49,6 +49,8 @@ skills/
## Reconciliation State
+**Scoped analysis (2026-09-29)**: `specs/web-console/gateway-provision-placement.spec.md` against `77feccc3` plus the current working tree: 10 Present. GP-W1 made REST creation intent-only and refreshed restored placement metadata; GP-W2 added localized, programmatically associated disabled-network reasons.
+
**Last analyzed**: 2026-10-01 (scoped reconciliation of specs/platform/e2e-testing.spec.md + specs/platform/e2e-console-browser-testing.spec.md against HEAD `addb9509` after #413 renumbered the core e2e suite 6 -> 14 areas. All new gaps were test-only (underlying API/CP/UI features already existed). **Executed waves E2E-W4 + CON-W2:** added e2e areas 12-14 to `tests/e2e/e2e-openshell.sh` (E2E-14 registration, E2E-16 reconnect+gRPC identity, E2E-17 promotion, E2E-18 reconciled status, E2E-19 admin inventory+phase) plus helpers in `lib.sh`/`drivers/kind.sh`, and console areas 7+11 (CON-E2E-14 typeahead+install-docs, CON-E2E-13 fleet-dashboard promotion). Verified on a local Kind cluster: full long run = **90 passed, 2 failed** (1 pre-existing area-9 developer-sandbox flake unrelated to this work; 1 promotion `Degraded` assertion since corrected). Short mode 20/0; console unit test 56/0; all shell unit tests pass; `bash -n` clean. **Two divergences recorded** (do not edit specs): D-E2E-DELETE-CP (gateway DELETE needs the CP, so "delete while disconnected" is infeasible -- E2E-16 covers create-convergence only) and D-E2E-DEGRADED (a failed rollout keeps the last-good workload serving and stays Running, not Degraded; the test asserts the never-moves-to-bad safety invariant). Browser areas (CON-E2E-13/14) are code-complete + unit-tested but not browser-run locally (agent-browser not installed); CI covers them. Deferred: E2E-15 multi-cluster (E2E-W5, needs second-CP deploy) and E2E-20 qualification (E2E-W6, infra-gated).) Also 2026-09-23 (HYPERSHELL-334 implementation rebased onto `b152182c`: standing CI secrets moved to AWS Secrets Manager + GitHub OIDC + ESO; test-tier principals seeded from ESO on CI-owned PR environments and kept until the environment is destroyed). Also 2026-09-23 (scoped reanalysis of specs/platform/ephemeral-pr-environments.spec.md after #342: PR-ENV-6 now also publishes the hypershell-realm Keycloak admin console URL with impersonation instructions; PR-ENV-8 hides the password form when the GitHub IdP is active; PR-ENV-9 adds the interactive admin console path via `view-users`/`query-users`/`impersonation` realm-management roles granted on first broker login - all implemented and Present). Also 2026-09-22 (scoped reanalysis + execution of specs/platform/e2e-console-browser-testing.spec.md: CON-W1 fully closed; 49/49 checks pass on Kind including sandbox create/delete via the PatternFly ActionsColumn row kebab + typed-name confirmation modal and TSL single-quote quoting fix in lib.sh; CI integration wired into `.github/workflows/e2e.yml`; spec added to coverage table as 100%). Prior 2026-09-22 (scoped reanalysis for commits since `464ec5e`: updated codebase commit to `5e14f29b` (external-db-only-redo HEAD: remove ManagedDatabase API); registered 6 new spec files (gateway-provision-outcomes, gateway-release-distribution, gateway-fleet-total-trend, gateway-sandbox-active-trends, hub-cluster-utilization-trends, openshell-branch-build); updated E2E-9 for 3-mode E2E_MODE split (#332); added E2E-13 for macOS CLI container wrapper (#335); added DM-3g/h/i for new Gateway schema fields; added OS-14/OS-15 for openshift-seed and openshift-test requirements; full-corpus recount pending for new specs). Prior 2026-09-16 (scoped reanalysis + execution of specs/platform/ephemeral-pr-environments.spec.md for HYPERSHELL-240 after the spec moved from continuous-deploy-for-life-of-PR to ephemeral-by-default with `/pr-extend` / `/pr-destroy`; D-E2E-OIDC closed as Present via PR-ENV-10; the last full-corpus analysis remains 2026-08-31). Prior 2026-09-14 scoped analysis of specs/platform/gateway-deletion-finalization.spec.md for HYPERSHELL-182; closed the no-silent-orphan gap G1: best-effort deletion failures for gateway-owned resources with no automatic recovery path -- leaked ClusterRoleBinding, leaked Keycloak gateway/console clients, and Keycloak clients skipped when the stored identity is unresolvable or the provisioner is deconfigured -- now emit a durable IncompleteFinalization Warning Event in the control-plane namespace instead of only logging; in-namespace sweep G2 already satisfied. Prior 2026-09-09 scoped reanalysis of e2e-testing.spec.md + local-development.spec.md against HEAD `21f02a0` for the new OpenShift E2E CI content added by the HYPERSHELL-240 docs commit: OpenShift driver unification #232/#244, dynamic namespace-GC timing, and the merge-queue Kind CI gate are all implemented; D-E2E-OIDC was then still listed as a divergence pending HYPERSHELL-240 (closed 2026-09-16). Prior 2026-09-04 scoped reanalysis of the CP-OBS-07 reconcile-queue metric changes after review; operational-dashboard through OP-DASH-20; OP-DASH-18 NaN fallback; OP-DASH-19 independent metric sources + partial failure; OP-DASH-20 section titles + header refresh consolidation; cluster memory/cpu/pods/nodes metrics; gateway-provision-time GPT-W1; registered-users complete; the last full-corpus analysis remains 2026-08-31)
**Spec corpus**: 56 spec files; the coverage table tracks 48 analyzed feature/spec groups after adding Ephemeral CI Secrets, Ephemeral Test Credentials, Console Browser Testing, Gateway Provision Outcomes, Gateway Release Distribution, Gateway Fleet Total Trend, Gateway Sandbox Active Trends, Hub Cluster Utilization Trends, and OpenShell Branch Build (9 new specs from commits `85927b3c`/`be0bf2ea`/`630a5ed1`/`2bdeece6`; full per-requirement analysis pending for the last 6)
**Codebase commit**: working tree (HYPERSHELL-334 standing CI secrets + test-tier credentials, rebased onto `b152182c`; prior `5e14f29b` feat: remove ManagedDatabase API), then e2e-console-browser-testing CON-W1 (`2bdeece6`), then #342 Keycloak admin-console impersonation (`d7f33594`)
@@ -58,7 +60,6 @@ skills/
| Domain | Specs | Requirements | Present | Partial | Missing | Deferred | Coverage |
|--------|-------|-------------|---------|---------|---------|----------|----------|
| Platform - Data Model | 1 | 12 | 11 | 1 | 0 | 0 | 96% |
-| Platform - Data Model (Kind Removal) | 1 | 20 | 0 | 0 | 20 | 0 | 0% |
| Platform - Control Plane | 1 | 13 | 8 | 1 | 4 | 0 | 65% |
| Platform - Gateway (core) | 1 | 18 | 12 | 3 | 3 | 0 | 75% |
| Platform - Gateway DB | 1 | 12 | 10 | 2 | 0 | 0 | 83% |
@@ -87,6 +88,7 @@ skills/
| Platform - Gateway Provision Time | 1 | 8 | 8 | 0 | 0 | 0 | 100% |
| Platform - Console Browser Testing | 1 | 12 | 12 | 0 | 0 | 0 | 100% |
| Web Console - Architecture | 1 | 28 | 21 | 5 | 2 | 0 | 86% |
+| Web Console - Gateway Provision Placement | 1 | 10 | 10 | 0 | 0 | 0 | 100% |
| Web Console - Operational Dashboard | 1 | 20 | 20 | 0 | 0 | 0 | 100% |
| Security - RBAC Enforcement | 1 | 13 | 11 | 0 | 0 | 2 | 85% |
| Standards | 13 | 0 | 0 | 0 | 0 | 0 | N/A |
@@ -96,8 +98,8 @@ skills/
| Platform - Gateway Sandbox Active Trends | 1 | ~10 | ? | ? | ? | ? | Pending |
| Platform - Hub Cluster Utilization Trends | 1 | ~10 | ? | ? | ? | ? | Pending |
| Platform - OpenShell Branch Build | 1 | ~12 | 0 | 0 | ~12 | 0 | 0% |
-| **TOTAL (analyzed rows)** | **43** | **384** | **310** | **22** | **47** | **5** | **81%** |
-| **TOTAL (all 48 groups)** | **48** | **~424** | **?** | **?** | **?** | **?** | **Pending** |
+| **TOTAL (analyzed rows)** | **43** | **374** | **320** | **22** | **27** | **5** | **86%** |
+| **TOTAL (all 49 groups)** | **49** | **~434** | **?** | **?** | **?** | **?** | **Pending** |
### Spec Dependency Order
@@ -121,35 +123,6 @@ Layer 7: web-console/architecture (depends on data-model, security, UI
## Gap Table
-### Kind Removal: GatewayRelease + GatewayNetwork
-
-These items track deletion of deprecated kinds across all implementation layers.
-Execution order is the reverse of creation: FE -> CLI -> CP (watcher+reconciler) -> BE+gRPC (plugin, handler, presenter, migration) -> SDK (generated types) -> API (OpenAPI routes+schema).
-Database tables are dropped via a final migration after confirmed zero rows.
-
-| # | Kind | Layer | Action | Status | Code Location | Wave |
-|---|------|-------|--------|--------|---------------|------|
-| RM-1 | GatewayRelease | FE | Remove any UI references (list/detail views, selectors) | Missing | `components/web-console/` | RM-W1 |
-| RM-2 | GatewayNetwork | FE | Remove any UI references | Missing | `components/web-console/` | RM-W1 |
-| RM-3 | GatewayRelease | CLI | Delete `hsctl create/get/list/delete gatewayRelease` commands | Missing | `components/cli/pkg/tui/rest.go`, `components/cli/` | RM-W2 |
-| RM-4 | GatewayNetwork | CLI | Delete `hsctl create/get/list/delete gatewayNetwork` commands | Missing | `components/cli/pkg/tui/rest.go`, `components/cli/` | RM-W2 |
-| RM-5 | GatewayRelease | CP | Delete `GatewayReleaseReconciler`, `WatchGatewayReleases`, `gateway_version_selection_test.go` | Missing | `components/control-plane/internal/reconciler/reconciler.go`, `watcher/watcher.go` | RM-W3 |
-| RM-6 | GatewayNetwork | CP | Delete `GatewayNetworkReconciler`, `WatchGatewayNetworks`, `gateway_network_test.go` | Missing | `components/control-plane/internal/reconciler/reconciler.go`, `watcher/watcher.go` | RM-W3 |
-| RM-7 | GatewayRelease | CP | Remove `release_id` propagation from `GatewayReconciler`; keep `image` path only | Missing | `components/control-plane/internal/reconciler/reconciler.go` | RM-W3 |
-| RM-8 | GatewayRelease | BE | Delete `components/api-server/plugins/gatewayReleases/` plugin entirely | Missing | `components/api-server/plugins/gatewayReleases/` | RM-W4 |
-| RM-9 | GatewayNetwork | BE | Delete `components/api-server/plugins/gatewayNetworks/` plugin entirely | Missing | `components/api-server/plugins/gatewayNetworks/` | RM-W4 |
-| RM-10 | GatewayRelease | gRPC | Delete `gateway_releases.proto`, regenerate stubs, delete `grpc_handler.go`, `grpc_presenter.go`, update `grpc_authorization.go` | Missing | `components/api-server/proto/`, `plugins/gatewayReleases/grpc_*` | RM-W4 |
-| RM-11 | GatewayNetwork | gRPC | Delete `gateway_networks.proto`, regenerate stubs, delete `grpc_handler.go`, `grpc_presenter.go`, update `grpc_authorization.go` | Missing | `components/api-server/proto/`, `plugins/gatewayNetworks/grpc_*` | RM-W4 |
-| RM-12 | GatewayRelease | API | Remove `/gateway_releases` routes and `GatewayRelease` schema from OpenAPI spec, regenerate | Missing | `components/api-server/openapi/` | RM-W5 |
-| RM-13 | GatewayNetwork | API | Remove `/gateway_networks` routes and `GatewayNetwork` schema from OpenAPI spec, regenerate | Missing | `components/api-server/openapi/` | RM-W5 |
-| RM-14 | GatewayRelease | SDK | Regenerate Go + TS SDKs (generated files will drop GatewayRelease types automatically) | Missing | `components/sdk-go/`, `components/sdk-typescript/` | RM-W5 |
-| RM-15 | GatewayNetwork | SDK | Regenerate Go + TS SDKs | Missing | `components/sdk-go/`, `components/sdk-typescript/` | RM-W5 |
-| RM-16 | GatewayRelease | DB | Add migration to drop `gateway_releases` table (after zero-row verification) | Missing | `components/api-server/plugins/gatewayReleases/migration.go` | RM-W6 |
-| RM-17 | GatewayNetwork | DB | Add migration to drop `gateway_networks` table | Missing | `components/api-server/plugins/gatewayNetworks/migration.go` | RM-W6 |
-| RM-18 | Gateway | BE | Remove `release_id` field from Gateway model, handler, and migration (nullable FK to removed table) | Missing | `components/api-server/plugins/gateways/` | RM-W4 |
-| RM-19 | Gateway | API | Remove `release_id` from OpenAPI Gateway schema, regenerate | Missing | `components/api-server/openapi/` | RM-W5 |
-| RM-20 | GatewayRelease+GatewayNetwork | RBAC | Remove both from `grpc_authorization.go` control-plane identity allow-list | Missing | `components/api-server/pkg/rbac/grpc_authorization.go` | RM-W4 |
-
### generated-gateway-config-validation.spec.md (HYPERSHELL-179)
| # | Requirement | Status | Gap | Code Location | Wave |
@@ -205,6 +178,29 @@ Database tables are dropped via a final migration after confirmed zero rows.
The first gap analysis found a race between the event-driven reconciler and the health reconciler. Both paths could record the first `Running` transition. CP-OBS-GPD-W1 adds one process-wide claim per Gateway. The delete path removes the claim. A normal reconcile uses the stored phase before work starts. A forced retry keeps the phase that existed before the retry bypass. These checks prevent work on a `Running` or `Degraded` Gateway from producing a new observation.
+### gateway-provision-placement.spec.md (HYPERSHELL-354)
+
+| # | Requirement | Status | Gap | Code Location | Wave |
+|---|-------------|--------|-----|---------------|------|
+| GP-01 | Placement intent contract; clients do not submit a concrete cluster | Present | REST create accepts placement intent only and always resolves it; concrete `cluster_id` remains read/internal compatibility data | `openapi/openapi.gateways.yaml`, `plugins/gateways/handler.go` | GP-W1 |
+| GP-02 | Mutually exclusive, required VPN/Public selection; no cluster selector | Present | The new radio-card form replaces the former selector; the obsolete selector file is removed in the current working tree | `packages/gateway-management-ui/src/gateways/gateway-create.tsx` | - |
+| GP-03 | Provider selection and VPN/AWS constraint | Present | Public prefers IBM when available; VPN forces AWS and disables IBM | `packages/gateway-management-ui/src/gateways/gateway-create.tsx` | - |
+| GP-04 | Placement availability from connected registered clusters | Present | Eligibility, automatic refresh, restored provider/visibility/liveness, and localized disabled-network reasons are present | `plugins/gateways/plugin.go`, `plugins/managedClusters/{service.go,dao.go}`, `gateway-create.tsx` | GP-W1, GP-W2 |
+| GP-05 | Kind-only local placement fallback | Present | API-server Kind flag, connected `local-kind` registration, managed-placement suppression, default selection, and intent submission are implemented | `plugins/gateways/plugin.go`, `gateway-create.tsx`, `deploy/kind/kustomization.yaml` | - |
+| GP-06 | Submit-time form validation with enabled action | Present | Name and placement validate on activation; the action is disabled only while a request is pending | `packages/gateway-management-ui/src/gateways/gateway-create.tsx` | - |
+| GP-07 | Random backend resolution without capacity scoring | Present | Cryptographic uniform index over provider/network/liveness-filtered candidates; no capacity input | `plugins/gateways/placement.go`, `placement_test.go` | - |
+| GP-08 | Typed local-kind resolution failures | Present | Kind mode and connected registration are rechecked server-side and failures use the standard client-visible validation error | `plugins/gateways/plugin.go` | - |
+| GP-09 | API, SDK, persistence, authorization, and read compatibility | Present | OpenAPI, Go/TS SDKs, registration data, resolver tests, and existing `cluster_id` reads are wired; gateway intent remains request-scoped | `openapi/`, `components/sdk-*`, `plugins/gateways/`, `plugins/managedClusters/` | - |
+| GP-10 | Accessibility and localization | Present | Localized native radio groups expose selected/disabled state, retain keyboard behavior, and associate each visible choice description with its input through `aria-describedby` | `gateway-create.tsx`, `messages.ts`, `components/web-console/locales/en.json` | GP-W2 |
+
+**Scoped coverage:** 10 of 10 requirements present after GP-W1 and GP-W2. The former cluster-selector component deletion already exists in the user's working tree and is preserved as part of the analyzed state.
+
+**Direction checks:**
+
+- Spec to code: all ten requirement areas have implementation and focused regression coverage.
+- Code to spec: REST creation is intent-only. Existing placement lookup operations remain necessary for rendering resolved cluster names on read surfaces.
+- OpenAPI to spec: placement is required and `cluster_id` is absent from the public REST create contract while remaining available on read models.
+
### openshell-gateway-console.spec.md
| # | Requirement | Status | Gap | Code Location | Wave |
@@ -918,6 +914,33 @@ Test-tier Keycloak principals `admin` / `developer` / `platform-admin` are not b
## Wave Plan
+### GP-W1: Enforce intent-only REST placement and restore placement capabilities
+
+**Scope:** GP-01, GP-04 (backend)
+
+**Dependency:** Existing placement OpenAPI, generated SDK, registration, and resolver implementation
+
+**Status:** Complete - source and generated contracts updated; affected packages compile. Runtime integration tests require a running Docker daemon for PostgreSQL testcontainers.
+
+1. Remove deprecated `cluster_id` from the public REST `GatewayCreateRequest` source contract and regenerate OpenAPI clients/SDKs.
+2. Make the REST create handler always validate and resolve `placement`; retain resolved `cluster_id` only on persisted/read models and internal update paths.
+3. Extend managed-cluster restore to atomically refresh provider, visibility, and `last_seen_at` from the current registration.
+4. Add regression tests for direct-cluster rejection/absence, intent resolution, and restored capability refresh.
+5. Verify generated artifacts, API build/vet, focused placement tests, and unchanged gateway read compatibility.
+
+### GP-W2: Explain disabled network placement to all users
+
+**Scope:** GP-04 (web), GP-10 verification
+
+**Dependency:** GP-W1
+
+**Status:** Complete - localized unavailability descriptions are visible and associated with each native radio input; reusable UI checks pass (200 tests).
+
+1. Give disabled Public/VPN controls a localized placement-specific reason when no provider is eligible.
+2. Associate choice descriptions with their semantic radio inputs so assistive technology receives the same reason as visual users.
+3. Add UI tests for disabled-network descriptions and accessible associations.
+4. Verify the reusable gateway UI package and web-console adapter/story checks.
+
### CGV-W1: Generated gateway configuration validation ✅
**Scope:** CGV-1 through CGV-4 (HYPERSHELL-179) | **Status:** Complete
@@ -1488,6 +1511,7 @@ label-selected pod informer.
| Date | Commit | Action | Coverage | Notes |
|------|--------|--------|----------|-------|
| 2026-10-02 | working tree | HYPERSHELL-250: retire legacy `components/pr-test/e2e-openshell.sh` | OS-7 Missing -> Present; PR-ENV-11 updated | Verified the shared harness (`tests/e2e/e2e-openshell.sh` areas 1-11) is a strict superset of the legacy script's 8 areas before removing it. Deleted `components/pr-test/e2e-openshell.sh`; repointed `lint-pr-test` in `.github/workflows/checks.yml` to syntax-check `e2e-openshell-roks.sh` (the `pr_test` component/CI wiring stays, rescoped to ROKS, since that script still lives under `components/pr-test/`); updated the "Legacy pr-test Deprecation" requirement in `ephemeral-pr-environments.spec.md` to "Legacy pr-test Removal" (and matching scenarios), plus mirrored language in `openshift-development.spec.md` and `e2e-testing.spec.md`; updated `CLAUDE.md`, `DEVELOPMENT.md`, and `skills/build/full-stack-pipeline/SKILL.md` to drop deprecation framing and point only at the shared harness. ROKS and GCP skill docs untouched. |
+| 2026-09-29 | `77feccc3` + working tree | Reconciled `gateway-provision-placement.spec.md` | 10/10 Present | Completed GP-W1 intent-only REST create and restored registration capability refresh, then GP-W2 localized disabled-network reasons with `aria-describedby`. API/SDK builds, vet, generated artifacts, UI checks (200 tests), and i18n extraction pass. Runtime API integration tests remain environment-blocked because Docker is not running for PostgreSQL testcontainers. |
| 2026-10-01 | working tree (on `addb9509`) | Waves E2E-W4 + CON-W2: close #413 e2e coverage gaps (areas 12-14 + console areas 7/11) | e2e-testing areas 6->14 (E2E-14/16/17/18/19 Present); console CON-E2E-13/14 Present | Added e2e areas 12-14 to `e2e-openshell.sh` (ManagedCluster registration, CP reconnect + gRPC identity rejection, release promotion, reconciled release/network status, admin `/v1/users` + unknown-phase), helpers in `lib.sh` (`e2e_json_field`, `e2e_poll_resource_status`, status predicates, new env defaults) and `drivers/kind.sh` (`acquire_registrar_token`); console areas 7 (typeahead + install-docs link) and 11 (fleet-dashboard promotion, skip-gated). Verified on local Kind: long run 90 passed / 2 failed (1 pre-existing area-9 flake; 1 promotion assertion since fixed), short 20/0, console unit 56/0, shell unit tests pass. Two divergences recorded (D-E2E-DELETE-CP: gateway DELETE needs the CP so delete-while-disconnected is infeasible; D-E2E-DEGRADED: failed rollout stays Running-on-last-good, not Degraded). Deferred: E2E-15 multi-cluster, E2E-20 qualification. Fixed a `set -e` abort bug (bare `$(e2e_wait_gateway_running/e2e_poll_resource_status)` assignments now `|| true`). |
| 2026-09-23 | working tree | HYPERSHELL-334: standing CI secrets + test-tier credentials | Ephemeral CI Secrets 5/5, Test Credentials 12/12, PR-ENV 100% | Moved cluster login and GitHub OAuth off Actions secrets onto AWS SM + GitHub OIDC + ESO. Realm no longer ships human test-tier passwords. CI-owned seed reads `hypershell-e2e-test-users`; seeded users last until namespace destroy; Kind/local keep admin/admin. Rebased onto `b152182c`. Ops must apply TF, put SM values, delete Actions secrets. |
| 2026-09-22 | `5e14f29b` | RECONCILE.md checkpoint update: registered 6 new spec files, updated E2E-9 for 3-mode split, added E2E-13 (macOS CLI container), DM-3g/h/i (new Gateway fields), OS-14/OS-15 (openshift-seed/test) | 84% (analyzed rows unchanged; 6 new specs pending full analysis) | Codebase commit advanced from `464ec5e` to `5e14f29b`. New specs from commits: `85927b3c` (gateway-provision-outcomes, gateway-release-distribution), `be0bf2ea` (gateway-fleet-total-trend, gateway-sandbox-active-trends, hub-cluster-utilization-trends), `630a5ed1` (openshell-branch-build - spec only, 0% implemented). |
From 40625a9fb36f31035b9270009de967994e3d9eba Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 30 Sep 2026 16:34:51 -0500
Subject: [PATCH 03/12] Add updated changes to mocks
---
specs/web-console/mockups/assets.d.ts | 5 +
.../gateways/provision-gateway.stories.tsx | 4 +
.../mockups/gateways/provision-gateway.tsx | 248 +++++++++++++-----
3 files changed, 193 insertions(+), 64 deletions(-)
diff --git a/specs/web-console/mockups/assets.d.ts b/specs/web-console/mockups/assets.d.ts
index abca4e784..c0c6ead35 100644
--- a/specs/web-console/mockups/assets.d.ts
+++ b/specs/web-console/mockups/assets.d.ts
@@ -3,6 +3,11 @@ declare module "*.png" {
export default source;
}
+declare module "*.svg" {
+ const source: string;
+ export default source;
+}
+
declare module "*.module.css" {
const classes: Record;
export default classes;
diff --git a/specs/web-console/mockups/gateways/provision-gateway.stories.tsx b/specs/web-console/mockups/gateways/provision-gateway.stories.tsx
index 88c7a5ddd..f7d592616 100644
--- a/specs/web-console/mockups/gateways/provision-gateway.stories.tsx
+++ b/specs/web-console/mockups/gateways/provision-gateway.stories.tsx
@@ -18,3 +18,7 @@ export const Default: Story = {};
export const ValidationErrors: Story = {
args: { showValidationErrors: true },
};
+
+export const LocalDevelopment: Story = {
+ args: { showLocalDevelopment: true },
+};
diff --git a/specs/web-console/mockups/gateways/provision-gateway.tsx b/specs/web-console/mockups/gateways/provision-gateway.tsx
index 38378bd46..d60be425d 100644
--- a/specs/web-console/mockups/gateways/provision-gateway.tsx
+++ b/specs/web-console/mockups/gateways/provision-gateway.tsx
@@ -1,27 +1,86 @@
import {
ActionGroup,
Button,
+ Card,
+ CardBody,
+ CardHeader,
+ CardTitle,
Form,
- FormHelperText,
FormGroup,
+ Gallery,
+ FormHelperText,
HelperText,
HelperTextItem,
- MenuToggle,
- Select,
- SelectOption,
- SelectList,
+ PageSection,
TextInput,
} from "@patternfly/react-core";
import { useState } from "react";
+import awsLogo from "../../../../packages/gateway-management-ui/src/assets/aws-logo.svg";
+import ibmCloudLogo from "../../../../packages/gateway-management-ui/src/assets/ibm-cloud.svg";
import { MockupTemplate } from "../shell/mockup-template";
+function Choice({
+ title,
+ description,
+ value,
+ selected,
+ icon,
+ isDisabled,
+ name,
+ onChoose,
+}: {
+ title: string;
+ description?: string;
+ value: string;
+ selected?: boolean;
+ icon?: string;
+ isDisabled?: boolean;
+ name: string;
+ onChoose: () => void;
+}) {
+ const labelId = `${name}-${value}-label`;
+ return (
+ {
+ if (!isDisabled) onChoose();
+ }}
+ >
+
+
+ {icon ? : null}{" "}
+ {title}
+
+
+ {description ? {description} : null}
+
+ );
+}
+
export function ProvisionGatewayMockup({
showValidationErrors = false,
+ showLocalDevelopment = false,
}: {
showValidationErrors?: boolean;
+ showLocalDevelopment?: boolean;
}) {
- const [isOpen, setIsOpen] = useState(false);
+ const [name, setName] = useState("");
+ const [network, setNetwork] = useState<"public" | "vpn">("public");
+ const [provider, setProvider] = useState<"aws" | "ibm">("ibm");
+ const [localKind, setLocalKind] = useState(showLocalDevelopment);
return (
-
+
);
}
From 0c3f540a92af77b3b055efcdcc5cf966e494332f Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 30 Sep 2026 21:13:32 -0500
Subject: [PATCH 04/12] Styled up form
---
components/web-console/locales/en.json | 8 +++--
.../src/gateways/gateway-create.module.css | 16 +++++++--
.../src/gateways/gateway-create.tsx | 27 +++++++++++---
.../gateway-management-ui/src/messages.ts | 10 ++++--
.../gateway-provision-placement.spec.md | 7 ++++
.../mockups/gateways/provision-gateway.tsx | 35 ++++++++++++++++---
6 files changed, 87 insertions(+), 16 deletions(-)
diff --git a/components/web-console/locales/en.json b/components/web-console/locales/en.json
index 49117e520..df6e97a8a 100644
--- a/components/web-console/locales/en.json
+++ b/components/web-console/locales/en.json
@@ -1584,7 +1584,7 @@
"description": "Public network placement option."
},
"app.page.gatewayProvision.publicNetworkDescription": {
- "defaultMessage": "Accessible through a public endpoint. Choose a cloud provider below.",
+ "defaultMessage": "Accessible through a public endpoint.",
"description": "Description of public network placement."
},
"app.page.gatewayProvision.title": {
@@ -1604,9 +1604,13 @@
"description": "VPN network placement option."
},
"app.page.gatewayProvision.vpnNetworkDescription": {
- "defaultMessage": "Private network access. AWS is required for VPN placement.",
+ "defaultMessage": "For gateways that need to reach GitLab and other Red Hat internal services.",
"description": "Description of VPN placement."
},
+ "app.page.gatewayProvision.vpnRequiresLabel": {
+ "defaultMessage": "Red Hat VPN required",
+ "description": "Label shown above the VPN placement guidance."
+ },
"app.page.gatewayProvision.vpnRequiresAws": {
"defaultMessage": "VPN placement requires AWS.",
"description": "Explanation shown when IBM Cloud is disabled for VPN."
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.module.css b/packages/gateway-management-ui/src/gateways/gateway-create.module.css
index 69176d685..abf1bf9da 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.module.css
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.module.css
@@ -12,10 +12,20 @@
min-height: 3rem;
}
.providerLogo {
- width: 7rem;
- height: 3rem;
+ display: block;
+ width: 6rem;
+ height: 2.5rem;
+ margin-bottom: 0.75rem;
object-fit: contain;
- object-position: left center;
+ object-position: center;
+}
+.providerContent {
+ display: block;
+ text-align: center;
+}
+.descriptionAfterLabel {
+ display: block;
+ margin-top: 0.25rem;
}
.help {
margin-top: 1.5rem;
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
index 7fc9c53af..5b64aa2da 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
@@ -13,6 +13,7 @@ import {
FormHelperText,
HelperText,
HelperTextItem,
+ Label,
PageSection,
Stack,
StackItem,
@@ -65,6 +66,7 @@ interface GatewayTextFieldProps {
function Choice({
title,
description,
+ descriptionLabel,
value,
selected,
icon,
@@ -74,6 +76,7 @@ function Choice({
}: {
title: string;
description?: string;
+ descriptionLabel?: string;
value: string;
selected?: boolean;
icon?: string;
@@ -113,9 +116,6 @@ function Choice({
>
- {icon ? (
-
- ) : null}
{title}
@@ -123,7 +123,23 @@ function Choice({
{description ? (
- {description}
+
+ {descriptionLabel ? (
+ {descriptionLabel}
+ ) : null}
+ {icon ? (
+
+
+ {description}
+
+ ) : descriptionLabel ? (
+
+ {description}
+
+ ) : (
+ description
+ )}
+
) : null}
);
@@ -475,6 +491,9 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
value="vpn"
selected={!localKind && field.value === "vpn"}
title={intl.formatMessage(messages.vpnNetwork)}
+ descriptionLabel={intl.formatMessage(
+ messages.vpnRequiresLabel,
+ )}
description={
availability.data && !vpnPlacementAvailable
? intl.formatMessage(messages.unavailableNetwork, {
diff --git a/packages/gateway-management-ui/src/messages.ts b/packages/gateway-management-ui/src/messages.ts
index a27328cde..16a81e2e3 100644
--- a/packages/gateway-management-ui/src/messages.ts
+++ b/packages/gateway-management-ui/src/messages.ts
@@ -524,8 +524,7 @@ export const messages = defineMessages({
},
publicNetworkDescription: {
id: "app.page.gatewayProvision.publicNetworkDescription",
- defaultMessage:
- "Accessible through a public endpoint. Choose a cloud provider below.",
+ defaultMessage: "Accessible through a public endpoint.",
description: "Description of public network placement.",
},
vpnNetwork: {
@@ -536,7 +535,7 @@ export const messages = defineMessages({
vpnNetworkDescription: {
id: "app.page.gatewayProvision.vpnNetworkDescription",
defaultMessage:
- "Private network access. AWS is required for VPN placement.",
+ "For gateways that need to reach GitLab and other Red Hat internal services.",
description: "Description of VPN placement.",
},
awsProvider: {
@@ -578,6 +577,11 @@ export const messages = defineMessages({
defaultMessage: "VPN placement requires AWS.",
description: "Explanation shown when IBM Cloud is disabled for VPN.",
},
+ vpnRequiresLabel: {
+ id: "app.page.gatewayProvision.vpnRequiresLabel",
+ defaultMessage: "Red Hat VPN required",
+ description: "Label shown above the VPN placement guidance.",
+ },
localKindPlacement: {
id: "app.page.gatewayProvision.localKindPlacement",
defaultMessage: "Use local-kind",
diff --git a/specs/web-console/gateway-provision-placement.spec.md b/specs/web-console/gateway-provision-placement.spec.md
index d574fc48f..7eeba6b1c 100644
--- a/specs/web-console/gateway-provision-placement.spec.md
+++ b/specs/web-console/gateway-provision-placement.spec.md
@@ -58,6 +58,13 @@ The form SHALL NOT present the former cluster selector.
The provision form SHALL present AWS and IBM Cloud as mutually exclusive choices. The form SHALL require exactly one provider before submission.
+The provider choices SHALL include the following user guidance:
+
+- IBM Cloud: "The default home for gateways. General-purpose workloads with no special network or data needs."
+- AWS: "For workloads that rely heavily on AWS services or data."
+
+The VPN network choice SHALL display the label "Red Hat VPN required" above the guidance "For gateways that need to reach GitLab and other Red Hat internal services."
+
IBM Cloud SHALL be selected by default when Public becomes active. When VPN becomes active, AWS SHALL be selected and IBM Cloud SHALL be disabled. The user SHALL NOT be able to submit VPN placement with IBM Cloud.
#### Scenario: Public defaults to IBM Cloud
diff --git a/specs/web-console/mockups/gateways/provision-gateway.tsx b/specs/web-console/mockups/gateways/provision-gateway.tsx
index d60be425d..d3d7bb928 100644
--- a/specs/web-console/mockups/gateways/provision-gateway.tsx
+++ b/specs/web-console/mockups/gateways/provision-gateway.tsx
@@ -11,6 +11,7 @@ import {
FormHelperText,
HelperText,
HelperTextItem,
+ Label,
PageSection,
TextInput,
} from "@patternfly/react-core";
@@ -23,6 +24,7 @@ import { MockupTemplate } from "../shell/mockup-template";
function Choice({
title,
description,
+ descriptionLabel,
value,
selected,
icon,
@@ -32,6 +34,7 @@ function Choice({
}: {
title: string;
description?: string;
+ descriptionLabel?: string;
value: string;
selected?: boolean;
icon?: string;
@@ -61,11 +64,32 @@ function Choice({
}}
>
- {icon ? : null}{" "}
{title}
- {description ? {description} : null}
+ {description ? (
+
+ {descriptionLabel ? (
+ {descriptionLabel}
+ ) : null}
+ {icon ? (
+
+
+ {description}
+
+ ) : (
+
+ {description}
+
+ )}
+
+ ) : null}
);
}
@@ -136,7 +160,7 @@ export function ProvisionGatewayMockup({
value="public"
selected={!localKind && network === "public"}
title="Public"
- description="Accessible through a public endpoint. Choose a cloud provider below."
+ description="Accessible through a public endpoint."
onChoose={() => {
setLocalKind(false);
setNetwork("public");
@@ -147,7 +171,8 @@ export function ProvisionGatewayMockup({
value="vpn"
selected={!localKind && network === "vpn"}
title="VPN"
- description="Private network access. AWS is required for VPN placement."
+ description="For gateways that need to reach GitLab and other Red Hat internal services."
+ descriptionLabel="Red Hat VPN required"
onChoose={() => {
setLocalKind(false);
setNetwork("vpn");
@@ -186,6 +211,7 @@ export function ProvisionGatewayMockup({
value="aws"
selected={provider === "aws"}
title="Amazon Web Services"
+ description="For workloads that rely heavily on AWS services or data."
icon={awsLogo}
onChoose={() => setProvider("aws")}
/>
@@ -194,6 +220,7 @@ export function ProvisionGatewayMockup({
value="ibm"
selected={provider === "ibm"}
title="IBM Cloud"
+ description="The default home for gateways. General-purpose workloads with no special network or data needs."
icon={ibmCloudLogo}
isDisabled={network === "vpn"}
onChoose={() => setProvider("ibm")}
From d750feeb41319f89e235e5a032f26304a239ae24 Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Thu, 1 Oct 2026 15:04:28 -0500
Subject: [PATCH 05/12] update ui code
---
.../gateways/gateway-create.stories.tsx | 50 +++++++++++++--
components/web-console/locales/en.json | 12 +++-
.../src/gateways/gateway-create.module.css | 7 ++-
.../src/gateways/gateway-create.test.tsx | 4 +-
.../src/gateways/gateway-create.tsx | 20 ++++--
.../gateway-management-ui/src/messages.ts | 15 ++++-
skills/web-console/ui-spec/ui-conventions.md | 8 +++
.../gateway-provision-placement.spec.md | 10 +--
.../mockups/gateways/provision-gateway.tsx | 63 ++++++++++++-------
specs/web-console/user_flows.md | 8 ++-
10 files changed, 150 insertions(+), 47 deletions(-)
diff --git a/components/web-console/app/features/gateways/gateway-create.stories.tsx b/components/web-console/app/features/gateways/gateway-create.stories.tsx
index 9bd8b73b2..a0bf2e653 100644
--- a/components/web-console/app/features/gateways/gateway-create.stories.tsx
+++ b/components/web-console/app/features/gateways/gateway-create.stories.tsx
@@ -18,6 +18,14 @@ const placementAvailability = {
localKind: true,
} as const;
+const localDevelopmentAvailability = {
+ awsPublic: false,
+ awsVpn: false,
+ ibmPublic: false,
+ ibmVpn: false,
+ localKind: true,
+} as const;
+
const previewGateway: GatewayRecord = {
clusterId: "preview-cluster",
id: "preview-gateway",
@@ -38,9 +46,20 @@ const navigation: GatewayUiNavigation = {
navigate: () => undefined,
};
-function GatewayCreatePreview() {
+function GatewayCreatePreview({
+ availability = placementAvailability,
+}: {
+ availability?: typeof placementAvailability;
+}) {
+ const operationsForStory = {
+ ...gatewayOperations,
+ getGatewayPlacementAvailability: () => Promise.resolve(availability),
+ } as GatewayOperations;
return (
-
+
);
@@ -67,13 +86,36 @@ export const ManagedPlacementsAvailable: Story = {
).toBeNull();
await expect(canvas.getByRole("radio", { name: "Public" })).toBeVisible();
await expect(canvas.getByRole("radio", { name: "VPN" })).toBeVisible();
- await expect(canvas.queryByRole("radio", { name: "AWS" })).toBeNull();
+ await expect(
+ canvas.queryByRole("radio", { name: "Amazon Web Services" }),
+ ).toBeNull();
await expect(canvas.queryByRole("radio", { name: "IBM Cloud" })).toBeNull();
await userEvent.click(canvas.getByRole("radio", { name: "Public" }));
- await expect(canvas.getByRole("radio", { name: "AWS" })).toBeVisible();
+ await expect(
+ canvas.getByRole("radio", { name: "Amazon Web Services" }),
+ ).toBeVisible();
await expect(
canvas.getByRole("radio", { name: "IBM Cloud" }),
).toBeVisible();
},
};
+
+export const LocalDevelopmentOnly: Story = {
+ render: () => (
+
+ ),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await expect(
+ canvas.getByRole("radio", { name: "Use local-kind" }),
+ ).toBeVisible();
+ await expect(canvas.queryByRole("radio", { name: "Public" })).toBeNull();
+ await expect(canvas.queryByRole("radio", { name: "VPN" })).toBeNull();
+ await expect(
+ canvas.queryByRole("radio", { name: "Amazon Web Services" }),
+ ).toBeNull();
+ await expect(canvas.queryByRole("radio", { name: "IBM Cloud" })).toBeNull();
+ },
+};
diff --git a/components/web-console/locales/en.json b/components/web-console/locales/en.json
index df6e97a8a..98a0f0b6b 100644
--- a/components/web-console/locales/en.json
+++ b/components/web-console/locales/en.json
@@ -1520,9 +1520,13 @@
"description": "Metadata title for a user-facing gateway details page."
},
"app.page.gatewayProvision.awsProvider": {
- "defaultMessage": "AWS",
+ "defaultMessage": "Amazon Web Services",
"description": "AWS cloud provider option."
},
+ "app.page.gatewayProvision.awsProviderDescription": {
+ "defaultMessage": "For workloads that rely heavily on AWS services or data.",
+ "description": "Description of the AWS provider option."
+ },
"app.page.gatewayProvision.cloudProvider": {
"defaultMessage": "Cloud provider",
"description": "Cloud provider for public gateway placement."
@@ -1543,6 +1547,10 @@
"defaultMessage": "IBM Cloud",
"description": "IBM Cloud provider option."
},
+ "app.page.gatewayProvision.ibmCloudProviderDescription": {
+ "defaultMessage": "The default home for gateways. General-purpose workloads with no special network or data needs.",
+ "description": "Description of the IBM Cloud provider option."
+ },
"app.page.gatewayProvision.localDevelopment": {
"defaultMessage": "Local development",
"description": "Form group label for local Kind placement."
@@ -1604,7 +1612,7 @@
"description": "VPN network placement option."
},
"app.page.gatewayProvision.vpnNetworkDescription": {
- "defaultMessage": "For gateways that need to reach GitLab and other Red Hat internal services.",
+ "defaultMessage": "For gateways that need to reach GitLab and other internal Red Hat services.",
"description": "Description of VPN placement."
},
"app.page.gatewayProvision.vpnRequiresLabel": {
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.module.css b/packages/gateway-management-ui/src/gateways/gateway-create.module.css
index abf1bf9da..b99e0b508 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.module.css
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.module.css
@@ -13,11 +13,12 @@
}
.providerLogo {
display: block;
- width: 6rem;
- height: 2.5rem;
- margin-bottom: 0.75rem;
+ width: 10rem;
+ height: 4rem;
+ margin: 0 auto 0.75rem;
object-fit: contain;
object-position: center;
+ box-sizing: content-box;
}
.providerContent {
display: block;
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
index c908658fc..611adde1f 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
@@ -92,7 +92,7 @@ describe("GatewayCreatePage", () => {
await enterNameAndSelectPublic(user);
expect(radio("IBM Cloud").checked).toBe(true);
- expect(radio("AWS").checked).toBe(false);
+ expect(radio("Amazon Web Services").checked).toBe(false);
await user.click(screen.getByRole("button", { name: "Provision gateway" }));
await waitFor(() => {
@@ -112,7 +112,7 @@ describe("GatewayCreatePage", () => {
);
await user.click(screen.getByRole("radio", { name: "VPN" }));
- expect(radio("AWS").checked).toBe(true);
+ expect(radio("Amazon Web Services").checked).toBe(true);
expect(radio("IBM Cloud").disabled).toBe(true);
await user.click(screen.getByRole("button", { name: "Provision gateway" }));
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
index 5b64aa2da..be58c6349 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
@@ -70,6 +70,7 @@ function Choice({
value,
selected,
icon,
+ iconPadding,
isDisabled,
name,
onChoose,
@@ -80,6 +81,7 @@ function Choice({
value: string;
selected?: boolean;
icon?: string;
+ iconPadding?: string;
isDisabled?: boolean;
name: string;
onChoose: () => void;
@@ -129,7 +131,12 @@ function Choice({
) : null}
{icon ? (
-
+
{description}
) : descriptionLabel ? (
@@ -394,7 +401,7 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
-
+
void;
@@ -78,7 +80,11 @@ function Choice({
alt=""
height="40"
src={icon}
- style={{ display: "block", margin: "0 auto 0.75rem" }}
+ style={{
+ display: "block",
+ margin: "0 auto 0.75rem",
+ padding: iconPadding,
+ }}
width="96"
/>
{description}
@@ -102,19 +108,23 @@ export function ProvisionGatewayMockup({
showLocalDevelopment?: boolean;
}) {
const [name, setName] = useState("");
- const [network, setNetwork] = useState<"public" | "vpn">("public");
- const [provider, setProvider] = useState<"aws" | "ibm">("ibm");
+ const [network, setNetwork] = useState<"public" | "vpn" | null>(
+ showLocalDevelopment ? null : "public",
+ );
+ const [provider, setProvider] = useState<"aws" | "ibm" | null>(
+ showLocalDevelopment ? null : "ibm",
+ );
const [localKind, setLocalKind] = useState(showLocalDevelopment);
return (
-
+
) : null}
-
+ {!showLocalDevelopment ? (
+
- {showLocalDevelopment ? (
- {
- setLocalKind(true);
- }}
- />
- ) : null}
{
setLocalKind(false);
@@ -194,7 +193,25 @@ export function ProvisionGatewayMockup({
-
+
+ ) : (
+
+
+ setLocalKind(true)}
+ />
+
+
+ )}
{network ? (
setProvider("aws")}
/>
) : null}
- A matching managed cluster is selected at random for the chosen
- network and provider.
+ {showLocalDevelopment
+ ? "The local Kind cluster is used for development."
+ : "A matching managed cluster is selected at random for the chosen network and provider."}
diff --git a/specs/web-console/user_flows.md b/specs/web-console/user_flows.md
index 0d9619e11..9ceb57e7e 100644
--- a/specs/web-console/user_flows.md
+++ b/specs/web-console/user_flows.md
@@ -153,7 +153,8 @@ flowchart LR
| Gateway name | All users with create permission | Required and validated before submission |
| Intended use - personal or team | All users with create permission | Proposed; needed only if personal and team ownership become different product concepts |
| Network visibility - VPN or Public | All users with create permission | Required; unavailable choices explain why they can't be selected |
-| Cloud provider - AWS or IBM Cloud | All users with create permission | Required; available providers depend on network visibility and placement availability |
+| Cloud provider - Amazon Web Services or IBM Cloud | All users with create permission | Required for managed placement; available providers depend on network visibility and placement availability |
+| Local development - Use local-kind | Kind development users with no managed placement available | Replaces network and cloud-provider selection when the connected local-kind cluster is available |
| Placement availability | All users with create permission | Shows whether a matching cluster is available without exposing the selected cluster ID |
| Team owner | Team users | Proposed; current authorization is based on individual creator and gateway bindings |
| Initial team members | Team users | Proposed; group access and team membership are not yet defined |
@@ -170,9 +171,12 @@ flowchart LR
login --> list["View my gateways"]
list --> start["Select Create gateway"]
start --> name["Enter gateway name"]
- name --> network["Select VPN or Public"]
+ name --> placement{"Managed placement available?"}
+ placement -- Yes --> network["Select VPN or Public"]
network --> provider["Select cloud provider"]
provider --> submit["Submit gateway"]
+ placement -- No, Kind local-kind available --> local["Select Use local-kind"]
+ local --> submit
submit --> valid{"Form valid?"}
valid -- No --> errors["Show field errors"]
errors --> name
From e51498102c0338f87e6a086a10c597fee7a3a507 Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Thu, 1 Oct 2026 15:28:01 -0500
Subject: [PATCH 06/12] amber recommended changes
---
.../openapi/openapi.managedClusters.yaml | 2 -
.../pkg/api/openapi/api/openapi.yaml | 2 -
.../pkg/api/openapi/docs/DefaultAPI.md | 2 +-
.../docs/ManagedClusterRegistrationRequest.md | 16 ++++-
...el_managed_cluster_registration_request.go | 70 ++++++++++++-------
.../api-server/plugins/gateways/placement.go | 5 +-
.../api-server/plugins/gateways/plugin.go | 8 ++-
.../plugins/managedClusters/handler.go | 18 +++--
.../plugins/managedClusters/presenter.go | 11 ++-
.../registration_integration_test.go | 55 ++++++++++++++-
.../plugins/managedClusters/service.go | 12 +++-
.../control-plane/internal/config/config.go | 10 +--
.../internal/config/config_test.go | 2 +
components/sdk-go/client/agent_runtime_api.go | 2 +-
components/sdk-go/client/client.go | 2 +-
components/sdk-go/client/gateway_api.go | 2 +-
.../sdk-go/client/inference_route_api.go | 2 +-
components/sdk-go/client/iterator.go | 2 +-
.../sdk-go/client/managed_cluster_api.go | 2 +-
.../open_shell_gateway_service_account_api.go | 2 +-
.../sdk-go/client/provider_binding_api.go | 2 +-
components/sdk-go/client/provider_spec_api.go | 2 +-
components/sdk-go/client/role_api.go | 2 +-
components/sdk-go/client/role_binding_api.go | 2 +-
.../sdk-go/client/sandbox_template_api.go | 2 +-
components/sdk-go/client/secret_source_api.go | 2 +-
components/sdk-go/client/user_api.go | 2 +-
components/sdk-go/types/agent_runtime.go | 2 +-
components/sdk-go/types/base.go | 2 +-
components/sdk-go/types/gateway.go | 2 +-
components/sdk-go/types/inference_route.go | 2 +-
components/sdk-go/types/list_options.go | 2 +-
components/sdk-go/types/managed_cluster.go | 2 +-
.../open_shell_gateway_service_account.go | 2 +-
components/sdk-go/types/provider_binding.go | 2 +-
components/sdk-go/types/provider_spec.go | 2 +-
components/sdk-go/types/role.go | 2 +-
components/sdk-go/types/role_binding.go | 2 +-
components/sdk-go/types/sandbox_template.go | 2 +-
components/sdk-go/types/secret_source.go | 2 +-
components/sdk-go/types/user.go | 2 +-
.../sdk-typescript/src/agent_runtime.ts | 2 +-
.../sdk-typescript/src/agent_runtime_api.ts | 2 +-
components/sdk-typescript/src/base.ts | 2 +-
components/sdk-typescript/src/client.ts | 2 +-
components/sdk-typescript/src/gateway.ts | 2 +-
components/sdk-typescript/src/gateway_api.ts | 2 +-
components/sdk-typescript/src/index.ts | 2 +-
.../sdk-typescript/src/inference_route.ts | 2 +-
.../sdk-typescript/src/inference_route_api.ts | 2 +-
.../sdk-typescript/src/managed_cluster.ts | 2 +-
.../sdk-typescript/src/managed_cluster_api.ts | 2 +-
.../src/open_shell_gateway_service_account.ts | 2 +-
.../open_shell_gateway_service_account_api.ts | 2 +-
.../sdk-typescript/src/provider_binding.ts | 2 +-
.../src/provider_binding_api.ts | 2 +-
.../sdk-typescript/src/provider_spec.ts | 2 +-
.../sdk-typescript/src/provider_spec_api.ts | 2 +-
components/sdk-typescript/src/role.ts | 2 +-
components/sdk-typescript/src/role_api.ts | 2 +-
components/sdk-typescript/src/role_binding.ts | 2 +-
.../sdk-typescript/src/role_binding_api.ts | 2 +-
.../sdk-typescript/src/sandbox_template.ts | 2 +-
.../src/sandbox_template_api.ts | 2 +-
.../sdk-typescript/src/secret_source.ts | 2 +-
.../sdk-typescript/src/secret_source_api.ts | 2 +-
components/sdk-typescript/src/user.ts | 2 +-
components/sdk-typescript/src/user_api.ts | 2 +-
.../gateways/gateway-create.stories.tsx | 12 ++--
components/web-console/locales/en.json | 8 +--
.../src/gateways/gateway-create.test.tsx | 17 +++--
.../src/gateways/gateway-create.tsx | 12 +++-
scripts/sdk-generator/parser_test.go | 7 ++
.../gateway-provision-placement.spec.md | 2 +-
74 files changed, 252 insertions(+), 129 deletions(-)
diff --git a/components/api-server/openapi/openapi.managedClusters.yaml b/components/api-server/openapi/openapi.managedClusters.yaml
index 6597dba4e..5efa023d5 100644
--- a/components/api-server/openapi/openapi.managedClusters.yaml
+++ b/components/api-server/openapi/openapi.managedClusters.yaml
@@ -328,8 +328,6 @@ components:
type: object
required:
- name
- - provider
- - visibility
properties:
name:
type: string
diff --git a/components/api-server/pkg/api/openapi/api/openapi.yaml b/components/api-server/pkg/api/openapi/api/openapi.yaml
index 9a249c7fe..e739bc88c 100644
--- a/components/api-server/pkg/api/openapi/api/openapi.yaml
+++ b/components/api-server/pkg/api/openapi/api/openapi.yaml
@@ -2998,8 +2998,6 @@ components:
type: string
required:
- name
- - provider
- - visibility
type: object
ManagedClusterRegistrationResponse:
example:
diff --git a/components/api-server/pkg/api/openapi/docs/DefaultAPI.md b/components/api-server/pkg/api/openapi/docs/DefaultAPI.md
index 025c0d1c7..6c8a06334 100644
--- a/components/api-server/pkg/api/openapi/docs/DefaultAPI.md
+++ b/components/api-server/pkg/api/openapi/docs/DefaultAPI.md
@@ -3203,7 +3203,7 @@ import (
)
func main() {
- managedClusterRegistrationRequest := *openapiclient.NewManagedClusterRegistrationRequest("Name_example", "Provider_example", "Visibility_example") // ManagedClusterRegistrationRequest | Registration request
+ managedClusterRegistrationRequest := *openapiclient.NewManagedClusterRegistrationRequest("Name_example") // ManagedClusterRegistrationRequest | Registration request
configuration := openapiclient.NewConfiguration()
apiClient := openapiclient.NewAPIClient(configuration)
diff --git a/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md b/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md
index 117db5917..ea4cec65e 100644
--- a/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md
+++ b/components/api-server/pkg/api/openapi/docs/ManagedClusterRegistrationRequest.md
@@ -6,14 +6,14 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**Name** | **string** | Human-readable spoke name, unique per fleet (e.g. hyp0-mc1). Must match on every subsequent call. |
**Description** | Pointer to **string** | Optional description of the spoke. | [optional]
-**Provider** | **string** | Cloud provider hosting this control plane. |
-**Visibility** | **string** | Network visibility supported for gateways on this control plane. |
+**Provider** | Pointer to **string** | Cloud provider hosting this control plane. | [optional]
+**Visibility** | Pointer to **string** | Network visibility supported for gateways on this control plane. | [optional]
## Methods
### NewManagedClusterRegistrationRequest
-`func NewManagedClusterRegistrationRequest(name string, provider string, visibility string, ) *ManagedClusterRegistrationRequest`
+`func NewManagedClusterRegistrationRequest(name string, ) *ManagedClusterRegistrationRequest`
NewManagedClusterRegistrationRequest instantiates a new ManagedClusterRegistrationRequest object
This constructor will assign default values to properties that have it defined,
@@ -92,6 +92,11 @@ and a boolean to check if the value has been set.
SetProvider sets Provider field to given value.
+### HasProvider
+
+`func (o *ManagedClusterRegistrationRequest) HasProvider() bool`
+
+HasProvider returns a boolean if a field has been set.
### GetVisibility
@@ -112,6 +117,11 @@ and a boolean to check if the value has been set.
SetVisibility sets Visibility field to given value.
+### HasVisibility
+
+`func (o *ManagedClusterRegistrationRequest) HasVisibility() bool`
+
+HasVisibility returns a boolean if a field has been set.
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
diff --git a/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go b/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go
index aa24983a3..3f253f8e2 100644
--- a/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go
+++ b/components/api-server/pkg/api/openapi/model_managed_cluster_registration_request.go
@@ -26,9 +26,9 @@ type ManagedClusterRegistrationRequest struct {
// Optional description of the spoke.
Description *string `json:"description,omitempty"`
// Cloud provider hosting this control plane.
- Provider string `json:"provider"`
+ Provider *string `json:"provider,omitempty"`
// Network visibility supported for gateways on this control plane.
- Visibility string `json:"visibility"`
+ Visibility *string `json:"visibility,omitempty"`
}
type _ManagedClusterRegistrationRequest ManagedClusterRegistrationRequest
@@ -37,11 +37,9 @@ type _ManagedClusterRegistrationRequest ManagedClusterRegistrationRequest
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
-func NewManagedClusterRegistrationRequest(name string, provider string, visibility string) *ManagedClusterRegistrationRequest {
+func NewManagedClusterRegistrationRequest(name string) *ManagedClusterRegistrationRequest {
this := ManagedClusterRegistrationRequest{}
this.Name = name
- this.Provider = provider
- this.Visibility = visibility
return &this
}
@@ -109,52 +107,68 @@ func (o *ManagedClusterRegistrationRequest) SetDescription(v string) {
o.Description = &v
}
-// GetProvider returns the Provider field value
+// GetProvider returns the Provider field value if set, zero value otherwise.
func (o *ManagedClusterRegistrationRequest) GetProvider() string {
- if o == nil {
+ if o == nil || IsNil(o.Provider) {
var ret string
return ret
}
-
- return o.Provider
+ return *o.Provider
}
-// GetProviderOk returns a tuple with the Provider field value
+// GetProviderOk returns a tuple with the Provider field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *ManagedClusterRegistrationRequest) GetProviderOk() (*string, bool) {
- if o == nil {
+ if o == nil || IsNil(o.Provider) {
return nil, false
}
- return &o.Provider, true
+ return o.Provider, true
+}
+
+// HasProvider returns a boolean if a field has been set.
+func (o *ManagedClusterRegistrationRequest) HasProvider() bool {
+ if o != nil && !IsNil(o.Provider) {
+ return true
+ }
+
+ return false
}
-// SetProvider sets field value
+// SetProvider gets a reference to the given string and assigns it to the Provider field.
func (o *ManagedClusterRegistrationRequest) SetProvider(v string) {
- o.Provider = v
+ o.Provider = &v
}
-// GetVisibility returns the Visibility field value
+// GetVisibility returns the Visibility field value if set, zero value otherwise.
func (o *ManagedClusterRegistrationRequest) GetVisibility() string {
- if o == nil {
+ if o == nil || IsNil(o.Visibility) {
var ret string
return ret
}
-
- return o.Visibility
+ return *o.Visibility
}
-// GetVisibilityOk returns a tuple with the Visibility field value
+// GetVisibilityOk returns a tuple with the Visibility field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *ManagedClusterRegistrationRequest) GetVisibilityOk() (*string, bool) {
- if o == nil {
+ if o == nil || IsNil(o.Visibility) {
return nil, false
}
- return &o.Visibility, true
+ return o.Visibility, true
+}
+
+// HasVisibility returns a boolean if a field has been set.
+func (o *ManagedClusterRegistrationRequest) HasVisibility() bool {
+ if o != nil && !IsNil(o.Visibility) {
+ return true
+ }
+
+ return false
}
-// SetVisibility sets field value
+// SetVisibility gets a reference to the given string and assigns it to the Visibility field.
func (o *ManagedClusterRegistrationRequest) SetVisibility(v string) {
- o.Visibility = v
+ o.Visibility = &v
}
func (o ManagedClusterRegistrationRequest) MarshalJSON() ([]byte, error) {
@@ -171,8 +185,12 @@ func (o ManagedClusterRegistrationRequest) ToMap() (map[string]interface{}, erro
if !IsNil(o.Description) {
toSerialize["description"] = o.Description
}
- toSerialize["provider"] = o.Provider
- toSerialize["visibility"] = o.Visibility
+ if !IsNil(o.Provider) {
+ toSerialize["provider"] = o.Provider
+ }
+ if !IsNil(o.Visibility) {
+ toSerialize["visibility"] = o.Visibility
+ }
return toSerialize, nil
}
@@ -182,8 +200,6 @@ func (o *ManagedClusterRegistrationRequest) UnmarshalJSON(data []byte) (err erro
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"name",
- "provider",
- "visibility",
}
allProperties := make(map[string]interface{})
diff --git a/components/api-server/plugins/gateways/placement.go b/components/api-server/plugins/gateways/placement.go
index 97b4ac60c..490f5ac61 100644
--- a/components/api-server/plugins/gateways/placement.go
+++ b/components/api-server/plugins/gateways/placement.go
@@ -2,11 +2,14 @@ package gateways
import (
"crypto/rand"
+ "errors"
"fmt"
"math/big"
"time"
)
+var errNoEligiblePlacement = errors.New("no eligible placement is available")
+
// PlacementIntent is the client-facing placement request. Cluster IDs are
// deliberately absent: they are selected by the API server at create time.
type PlacementIntent struct {
@@ -67,7 +70,7 @@ func resolvePlacement(intent PlacementIntent, candidates []PlacementCandidate, s
}
eligible := eligiblePlacementCandidates(intent, candidates)
if len(eligible) == 0 {
- return "", fmt.Errorf("no eligible placement is available")
+ return "", errNoEligiblePlacement
}
index, err := selectIndex(len(eligible))
if err != nil {
diff --git a/components/api-server/plugins/gateways/plugin.go b/components/api-server/plugins/gateways/plugin.go
index d29fb92b1..600224c31 100644
--- a/components/api-server/plugins/gateways/plugin.go
+++ b/components/api-server/plugins/gateways/plugin.go
@@ -2,12 +2,14 @@ package gateways
import (
"context"
+ stderrors "errors"
"fmt"
"net/http"
"os"
"strings"
"time"
+ "github.com/golang/glog"
"github.com/gorilla/mux"
"google.golang.org/grpc"
@@ -212,7 +214,11 @@ func (p *registeredPlacementService) Resolve(ctx context.Context, intent openapi
}
selected, err := p.selectManaged(snapshot, *intent.Network, *intent.Provider)
if err != nil {
- return "", errors.Validation("no eligible managed cluster is available for the requested placement")
+ if stderrors.Is(err, errNoEligiblePlacement) {
+ return "", errors.Validation("no eligible managed cluster is available for the requested placement")
+ }
+ glog.Errorf("failed to resolve managed cluster placement provider=%s network=%s: %v", *intent.Provider, *intent.Network, err)
+ return "", errors.GeneralError("failed to resolve managed cluster placement")
}
return selected, nil
}
diff --git a/components/api-server/plugins/managedClusters/handler.go b/components/api-server/plugins/managedClusters/handler.go
index 708eea1e2..f0d477133 100644
--- a/components/api-server/plugins/managedClusters/handler.go
+++ b/components/api-server/plugins/managedClusters/handler.go
@@ -59,15 +59,25 @@ func (h managedClusterHandler) Register(w http.ResponseWriter, r *http.Request)
))
return
}
- if req.GetProvider() != "aws" && req.GetProvider() != "ibm" && req.GetProvider() != "kind" {
+ provider := req.GetProvider()
+ visibility := req.GetVisibility()
+ // Provider and visibility were added after managed-cluster registration was
+ // already deployed. Accept an omitted pair while older control planes are
+ // still posting heartbeats; the service preserves any placement metadata
+ // previously supplied by a newer client.
+ if (provider == "") != (visibility == "") {
+ handlers.HandleError(r.Context(), w, errors.Validation("provider and visibility must be supplied together"))
+ return
+ }
+ if provider != "" && provider != "aws" && provider != "ibm" && provider != "kind" {
handlers.HandleError(r.Context(), w, errors.Validation("provider must be aws, ibm, or kind"))
return
}
- if req.GetVisibility() != "public" && req.GetVisibility() != "vpn" {
+ if visibility != "" && visibility != "public" && visibility != "vpn" {
handlers.HandleError(r.Context(), w, errors.Validation("visibility must be public or vpn"))
return
}
- if req.GetProvider() == "ibm" && req.GetVisibility() == "vpn" {
+ if provider == "ibm" && visibility == "vpn" {
handlers.HandleError(r.Context(), w, errors.Validation("IBM placement does not support VPN visibility"))
return
}
@@ -94,7 +104,7 @@ func (h managedClusterHandler) Register(w http.ResponseWriter, r *http.Request)
description = *req.Description
}
- cluster, created, svcErr := h.managedCluster.Register(ctx, req.Name, description, req.GetProvider(), req.GetVisibility(), oidcSubject)
+ cluster, created, svcErr := h.managedCluster.Register(ctx, req.Name, description, provider, visibility, oidcSubject)
if svcErr != nil {
handlers.HandleError(r.Context(), w, svcErr)
return
diff --git a/components/api-server/plugins/managedClusters/presenter.go b/components/api-server/plugins/managedClusters/presenter.go
index 69c31b364..a1d857e77 100644
--- a/components/api-server/plugins/managedClusters/presenter.go
+++ b/components/api-server/plugins/managedClusters/presenter.go
@@ -31,6 +31,10 @@ func ConvertManagedCluster(managedCluster openapi.ManagedCluster) *ManagedCluste
func PresentManagedCluster(managedCluster *ManagedCluster) openapi.ManagedCluster {
reference := presenters.PresentReference(managedCluster.ID, managedCluster)
+ var visibility *string
+ if managedCluster.Visibility != "" {
+ visibility = &managedCluster.Visibility
+ }
result := openapi.ManagedCluster{
Id: reference.Id,
Kind: reference.Kind,
@@ -39,12 +43,7 @@ func PresentManagedCluster(managedCluster *ManagedCluster) openapi.ManagedCluste
UpdatedAt: openapi.PtrTime(managedCluster.UpdatedAt),
Name: managedCluster.Name,
Provider: managedCluster.Provider,
- Visibility: func() *string {
- if managedCluster.Visibility == "" {
- return nil
- }
- return &managedCluster.Visibility
- }(),
+ Visibility: visibility,
Region: managedCluster.Region,
KubeconfigSecret: managedCluster.KubeconfigSecret,
Status: managedCluster.Status,
diff --git a/components/api-server/plugins/managedClusters/registration_integration_test.go b/components/api-server/plugins/managedClusters/registration_integration_test.go
index ac2b5e72e..a68552f74 100644
--- a/components/api-server/plugins/managedClusters/registration_integration_test.go
+++ b/components/api-server/plugins/managedClusters/registration_integration_test.go
@@ -23,7 +23,7 @@ func controlPlaneContext(h *test.Helper, subject string) context.Context {
func register(client *openapi.APIClient, ctx context.Context, name string) (*openapi.ManagedClusterRegistrationResponse, *http.Response, error) {
return client.DefaultAPI.RegisterManagedCluster(ctx).
- ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{Name: name, Provider: "aws", Visibility: "public"}).
+ ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{Name: name, Provider: openapi.PtrString("aws"), Visibility: openapi.PtrString("public")}).
Execute()
}
@@ -58,6 +58,55 @@ func TestManagedClusterRegistrationIdempotent(t *testing.T) {
Expect(resp.StatusCode).To(Equal(http.StatusConflict))
}
+func TestManagedClusterRegistrationAcceptsLegacyHeartbeatWithoutErasingPlacement(t *testing.T) {
+ h, client := test.RegisterIntegration(t)
+ ctx := controlPlaneContext(h, uniqueName("cp"))
+ name := uniqueName("hyp0-mc")
+
+ legacy := openapi.ManagedClusterRegistrationRequest{Name: name}
+ first, resp, err := client.DefaultAPI.RegisterManagedCluster(ctx).
+ ManagedClusterRegistrationRequest(legacy).
+ Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusCreated))
+
+ _, resp, err = client.DefaultAPI.RegisterManagedCluster(ctx).
+ ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{
+ Name: name,
+ Provider: openapi.PtrString("ibm"),
+ Visibility: openapi.PtrString("public"),
+ }).
+ Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
+
+ _, resp, err = client.DefaultAPI.RegisterManagedCluster(ctx).
+ ManagedClusterRegistrationRequest(legacy).
+ Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
+
+ cluster, _, err := client.DefaultAPI.GetManagedCluster(ctx, first.ClusterId).Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cluster.Provider).To(Equal("ibm"))
+ Expect(cluster.GetVisibility()).To(Equal("public"))
+
+ resp, err = client.DefaultAPI.DeleteManagedCluster(ctx, first.ClusterId).Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusNoContent))
+
+ _, resp, err = client.DefaultAPI.RegisterManagedCluster(ctx).
+ ManagedClusterRegistrationRequest(legacy).
+ Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(resp.StatusCode).To(Equal(http.StatusCreated))
+
+ cluster, _, err = client.DefaultAPI.GetManagedCluster(ctx, first.ClusterId).Execute()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cluster.Provider).To(Equal("ibm"))
+ Expect(cluster.GetVisibility()).To(Equal("public"))
+}
+
// Re-registration restores placement metadata from the current registration,
// rather than retaining the provider and visibility from the deleted record.
func TestManagedClusterRegistrationRestoreRefreshesPlacementMetadata(t *testing.T) {
@@ -76,8 +125,8 @@ func TestManagedClusterRegistrationRestoreRefreshesPlacementMetadata(t *testing.
restored, resp, err := client.DefaultAPI.RegisterManagedCluster(ctx).
ManagedClusterRegistrationRequest(openapi.ManagedClusterRegistrationRequest{
Name: name,
- Provider: "ibm",
- Visibility: "public",
+ Provider: openapi.PtrString("ibm"),
+ Visibility: openapi.PtrString("public"),
}).Execute()
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusCreated))
diff --git a/components/api-server/plugins/managedClusters/service.go b/components/api-server/plugins/managedClusters/service.go
index b8599f319..512324d44 100644
--- a/components/api-server/plugins/managedClusters/service.go
+++ b/components/api-server/plugins/managedClusters/service.go
@@ -187,8 +187,12 @@ func (s *sqlManagedClusterService) Register(ctx context.Context, name, descripti
}
now := time.Now()
existing.LastSeenAt = &now
- existing.Provider = provider
- existing.Visibility = visibility
+ // A legacy control plane omits placement metadata. Its heartbeat must
+ // refresh connectivity without erasing values from a newer registration.
+ if provider != "" {
+ existing.Provider = provider
+ existing.Visibility = visibility
+ }
updated, replaceErr := s.managedClusterDao.Replace(ctx, existing)
if replaceErr != nil {
return nil, false, services.HandleUpdateError("ManagedCluster", replaceErr)
@@ -221,6 +225,10 @@ func (s *sqlManagedClusterService) Register(ctx context.Context, name, descripti
return nil, false, errors.GeneralError("registration lookup failed: %s", delErr)
}
if deleted != nil {
+ if provider == "" {
+ provider = deleted.Provider
+ visibility = deleted.Visibility
+ }
restored, restoreErr := s.managedClusterDao.Restore(ctx, deleted.ID, provider, visibility, now)
if restoreErr != nil {
if isUniqueViolation(restoreErr) {
diff --git a/components/control-plane/internal/config/config.go b/components/control-plane/internal/config/config.go
index 0a618b6a2..c8477cf0c 100644
--- a/components/control-plane/internal/config/config.go
+++ b/components/control-plane/internal/config/config.go
@@ -144,17 +144,17 @@ func Load() (*Config, error) {
{"OIDC_CLIENT_ID", cfg.OIDCClientID},
{"OIDC_CLIENT_SECRET", cfg.OIDCClientSecret},
}
+ for _, r := range required {
+ if strings.TrimSpace(r.value) == "" {
+ return nil, fmt.Errorf("%s is required: every control plane registers with the hub as a managed cluster (specs/platform/control-plane.spec.md, Mandatory Cluster Identity)", r.name)
+ }
+ }
if (cfg.ManagedClusterProvider != "aws" && cfg.ManagedClusterProvider != "ibm" && cfg.ManagedClusterProvider != "kind") ||
(cfg.ManagedClusterVisibility != "public" && cfg.ManagedClusterVisibility != "vpn") ||
(cfg.ManagedClusterProvider == "ibm" && cfg.ManagedClusterVisibility == "vpn") ||
(cfg.ManagedClusterProvider == "kind" && cfg.ManagedClusterVisibility != "public") {
return nil, fmt.Errorf("invalid managed cluster placement: provider=%q visibility=%q", cfg.ManagedClusterProvider, cfg.ManagedClusterVisibility)
}
- for _, r := range required {
- if strings.TrimSpace(r.value) == "" {
- return nil, fmt.Errorf("%s is required: every control plane registers with the hub as a managed cluster (specs/platform/control-plane.spec.md, Mandatory Cluster Identity)", r.name)
- }
- }
// An invalid GATEWAY_RESOURCES fails startup rather than falling back to the
// defaults like the other tunables: silently ignoring an operator's sizing
diff --git a/components/control-plane/internal/config/config_test.go b/components/control-plane/internal/config/config_test.go
index e1016fcde..a6c2b2747 100644
--- a/components/control-plane/internal/config/config_test.go
+++ b/components/control-plane/internal/config/config_test.go
@@ -25,6 +25,8 @@ func setRequiredEnv(t *testing.T) {
func TestLoadRequiresClusterIdentity(t *testing.T) {
for _, name := range []string{
"HYPERSHELL_MANAGED_CLUSTER_NAME",
+ "HYPERSHELL_MANAGED_CLUSTER_PROVIDER",
+ "HYPERSHELL_MANAGED_CLUSTER_VISIBILITY",
"OIDC_ISSUER",
"OIDC_CLIENT_ID",
"OIDC_CLIENT_SECRET",
diff --git a/components/sdk-go/client/agent_runtime_api.go b/components/sdk-go/client/agent_runtime_api.go
index ac0a78abc..5ef17dbbb 100644
--- a/components/sdk-go/client/agent_runtime_api.go
+++ b/components/sdk-go/client/agent_runtime_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/client.go b/components/sdk-go/client/client.go
index fd3d71acd..60f9c16dd 100644
--- a/components/sdk-go/client/client.go
+++ b/components/sdk-go/client/client.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/gateway_api.go b/components/sdk-go/client/gateway_api.go
index 1fe9144bf..810fc2f8d 100644
--- a/components/sdk-go/client/gateway_api.go
+++ b/components/sdk-go/client/gateway_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/inference_route_api.go b/components/sdk-go/client/inference_route_api.go
index b94b5a7f8..7d770df5a 100644
--- a/components/sdk-go/client/inference_route_api.go
+++ b/components/sdk-go/client/inference_route_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/iterator.go b/components/sdk-go/client/iterator.go
index fed188840..a0e3ca9ef 100644
--- a/components/sdk-go/client/iterator.go
+++ b/components/sdk-go/client/iterator.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/managed_cluster_api.go b/components/sdk-go/client/managed_cluster_api.go
index 43fb489fd..45869c2f6 100644
--- a/components/sdk-go/client/managed_cluster_api.go
+++ b/components/sdk-go/client/managed_cluster_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/open_shell_gateway_service_account_api.go b/components/sdk-go/client/open_shell_gateway_service_account_api.go
index 8908955a5..481f50e84 100644
--- a/components/sdk-go/client/open_shell_gateway_service_account_api.go
+++ b/components/sdk-go/client/open_shell_gateway_service_account_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/provider_binding_api.go b/components/sdk-go/client/provider_binding_api.go
index c539fed5a..55fbd5cb2 100644
--- a/components/sdk-go/client/provider_binding_api.go
+++ b/components/sdk-go/client/provider_binding_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/provider_spec_api.go b/components/sdk-go/client/provider_spec_api.go
index f8c3df513..ba9c3b810 100644
--- a/components/sdk-go/client/provider_spec_api.go
+++ b/components/sdk-go/client/provider_spec_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/role_api.go b/components/sdk-go/client/role_api.go
index adfb88802..ebfef98dd 100644
--- a/components/sdk-go/client/role_api.go
+++ b/components/sdk-go/client/role_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/role_binding_api.go b/components/sdk-go/client/role_binding_api.go
index c9259a293..293336a0b 100644
--- a/components/sdk-go/client/role_binding_api.go
+++ b/components/sdk-go/client/role_binding_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/sandbox_template_api.go b/components/sdk-go/client/sandbox_template_api.go
index 310df29ba..ddebeabdf 100644
--- a/components/sdk-go/client/sandbox_template_api.go
+++ b/components/sdk-go/client/sandbox_template_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/secret_source_api.go b/components/sdk-go/client/secret_source_api.go
index ea473ef3c..a1b36adef 100644
--- a/components/sdk-go/client/secret_source_api.go
+++ b/components/sdk-go/client/secret_source_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/client/user_api.go b/components/sdk-go/client/user_api.go
index 92f87b2a6..89090e27e 100644
--- a/components/sdk-go/client/user_api.go
+++ b/components/sdk-go/client/user_api.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package client
diff --git a/components/sdk-go/types/agent_runtime.go b/components/sdk-go/types/agent_runtime.go
index b880acbe2..eec996a6e 100644
--- a/components/sdk-go/types/agent_runtime.go
+++ b/components/sdk-go/types/agent_runtime.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/base.go b/components/sdk-go/types/base.go
index d48a83fc7..4eb1af4ef 100644
--- a/components/sdk-go/types/base.go
+++ b/components/sdk-go/types/base.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/gateway.go b/components/sdk-go/types/gateway.go
index a8e16cb90..1f5f44841 100644
--- a/components/sdk-go/types/gateway.go
+++ b/components/sdk-go/types/gateway.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/inference_route.go b/components/sdk-go/types/inference_route.go
index 67c73fa48..9b44c53f1 100644
--- a/components/sdk-go/types/inference_route.go
+++ b/components/sdk-go/types/inference_route.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/list_options.go b/components/sdk-go/types/list_options.go
index 5a46974ad..e1cfc1347 100644
--- a/components/sdk-go/types/list_options.go
+++ b/components/sdk-go/types/list_options.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/managed_cluster.go b/components/sdk-go/types/managed_cluster.go
index ca29d68b5..e51e2e2eb 100644
--- a/components/sdk-go/types/managed_cluster.go
+++ b/components/sdk-go/types/managed_cluster.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/open_shell_gateway_service_account.go b/components/sdk-go/types/open_shell_gateway_service_account.go
index e1ff22a04..c25321be9 100644
--- a/components/sdk-go/types/open_shell_gateway_service_account.go
+++ b/components/sdk-go/types/open_shell_gateway_service_account.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/provider_binding.go b/components/sdk-go/types/provider_binding.go
index d504db99e..a2ce63a9f 100644
--- a/components/sdk-go/types/provider_binding.go
+++ b/components/sdk-go/types/provider_binding.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/provider_spec.go b/components/sdk-go/types/provider_spec.go
index 358aeda3d..802f844a0 100644
--- a/components/sdk-go/types/provider_spec.go
+++ b/components/sdk-go/types/provider_spec.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/role.go b/components/sdk-go/types/role.go
index 5cc0d3a56..2eb79bfc0 100644
--- a/components/sdk-go/types/role.go
+++ b/components/sdk-go/types/role.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/role_binding.go b/components/sdk-go/types/role_binding.go
index 95d8295ef..19516384b 100644
--- a/components/sdk-go/types/role_binding.go
+++ b/components/sdk-go/types/role_binding.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/sandbox_template.go b/components/sdk-go/types/sandbox_template.go
index 5ceb3a3c0..fc1e0a92d 100644
--- a/components/sdk-go/types/sandbox_template.go
+++ b/components/sdk-go/types/sandbox_template.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/secret_source.go b/components/sdk-go/types/secret_source.go
index 038e33b40..c81f80a27 100644
--- a/components/sdk-go/types/secret_source.go
+++ b/components/sdk-go/types/secret_source.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-go/types/user.go b/components/sdk-go/types/user.go
index a1dda713c..9d083c20c 100644
--- a/components/sdk-go/types/user.go
+++ b/components/sdk-go/types/user.go
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
package types
diff --git a/components/sdk-typescript/src/agent_runtime.ts b/components/sdk-typescript/src/agent_runtime.ts
index ef27d7ebc..76471d473 100644
--- a/components/sdk-typescript/src/agent_runtime.ts
+++ b/components/sdk-typescript/src/agent_runtime.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/agent_runtime_api.ts b/components/sdk-typescript/src/agent_runtime_api.ts
index f97f71dbb..5d0c6d8d3 100644
--- a/components/sdk-typescript/src/agent_runtime_api.ts
+++ b/components/sdk-typescript/src/agent_runtime_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/base.ts b/components/sdk-typescript/src/base.ts
index 96aa37942..248c52f24 100644
--- a/components/sdk-typescript/src/base.ts
+++ b/components/sdk-typescript/src/base.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
export type ObjectReference = {
id: string;
diff --git a/components/sdk-typescript/src/client.ts b/components/sdk-typescript/src/client.ts
index 431865a92..f37715bdb 100644
--- a/components/sdk-typescript/src/client.ts
+++ b/components/sdk-typescript/src/client.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig } from './base.js';
import { AgentRuntimeAPI } from './agent_runtime_api.js';
diff --git a/components/sdk-typescript/src/gateway.ts b/components/sdk-typescript/src/gateway.ts
index a66135239..9976499b5 100644
--- a/components/sdk-typescript/src/gateway.ts
+++ b/components/sdk-typescript/src/gateway.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
export type GatewayPlacementAvailability = {
diff --git a/components/sdk-typescript/src/gateway_api.ts b/components/sdk-typescript/src/gateway_api.ts
index f2cccc54e..b640509eb 100644
--- a/components/sdk-typescript/src/gateway_api.ts
+++ b/components/sdk-typescript/src/gateway_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/index.ts b/components/sdk-typescript/src/index.ts
index 06a157a61..349780669 100644
--- a/components/sdk-typescript/src/index.ts
+++ b/components/sdk-typescript/src/index.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
export { SDKClient } from './client.js';
export type { SDKClientConfig, ListOptions, RequestOptions, ObjectReference, ListMeta, APIError } from './base.js';
diff --git a/components/sdk-typescript/src/inference_route.ts b/components/sdk-typescript/src/inference_route.ts
index 2c118611e..f4473b185 100644
--- a/components/sdk-typescript/src/inference_route.ts
+++ b/components/sdk-typescript/src/inference_route.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/inference_route_api.ts b/components/sdk-typescript/src/inference_route_api.ts
index 07d3509b0..53e4b37aa 100644
--- a/components/sdk-typescript/src/inference_route_api.ts
+++ b/components/sdk-typescript/src/inference_route_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/managed_cluster.ts b/components/sdk-typescript/src/managed_cluster.ts
index 2f30efb90..be7c25483 100644
--- a/components/sdk-typescript/src/managed_cluster.ts
+++ b/components/sdk-typescript/src/managed_cluster.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/managed_cluster_api.ts b/components/sdk-typescript/src/managed_cluster_api.ts
index f8ba2c219..b3c33139e 100644
--- a/components/sdk-typescript/src/managed_cluster_api.ts
+++ b/components/sdk-typescript/src/managed_cluster_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/open_shell_gateway_service_account.ts b/components/sdk-typescript/src/open_shell_gateway_service_account.ts
index b9768c44b..208a58543 100644
--- a/components/sdk-typescript/src/open_shell_gateway_service_account.ts
+++ b/components/sdk-typescript/src/open_shell_gateway_service_account.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
export type OpenShellGatewayServiceAccountCapabilities = {
diff --git a/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts b/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts
index e1628afb0..96bec4aac 100644
--- a/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts
+++ b/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, RequestOptions } from './base.js';
import { sdkFetch } from './base.js';
diff --git a/components/sdk-typescript/src/provider_binding.ts b/components/sdk-typescript/src/provider_binding.ts
index 99b100649..a25ca4bbf 100644
--- a/components/sdk-typescript/src/provider_binding.ts
+++ b/components/sdk-typescript/src/provider_binding.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/provider_binding_api.ts b/components/sdk-typescript/src/provider_binding_api.ts
index cfc28fbce..efabea287 100644
--- a/components/sdk-typescript/src/provider_binding_api.ts
+++ b/components/sdk-typescript/src/provider_binding_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/provider_spec.ts b/components/sdk-typescript/src/provider_spec.ts
index 20898fd8c..1fe81f2f5 100644
--- a/components/sdk-typescript/src/provider_spec.ts
+++ b/components/sdk-typescript/src/provider_spec.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/provider_spec_api.ts b/components/sdk-typescript/src/provider_spec_api.ts
index 57885c894..4b588b7b4 100644
--- a/components/sdk-typescript/src/provider_spec_api.ts
+++ b/components/sdk-typescript/src/provider_spec_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/role.ts b/components/sdk-typescript/src/role.ts
index ee0a8d75a..2a9882186 100644
--- a/components/sdk-typescript/src/role.ts
+++ b/components/sdk-typescript/src/role.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/role_api.ts b/components/sdk-typescript/src/role_api.ts
index f6b4fe72e..e514c4b28 100644
--- a/components/sdk-typescript/src/role_api.ts
+++ b/components/sdk-typescript/src/role_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/role_binding.ts b/components/sdk-typescript/src/role_binding.ts
index 3a7c4f585..f59522d16 100644
--- a/components/sdk-typescript/src/role_binding.ts
+++ b/components/sdk-typescript/src/role_binding.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/role_binding_api.ts b/components/sdk-typescript/src/role_binding_api.ts
index 9a581f03d..b34d1677d 100644
--- a/components/sdk-typescript/src/role_binding_api.ts
+++ b/components/sdk-typescript/src/role_binding_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/sandbox_template.ts b/components/sdk-typescript/src/sandbox_template.ts
index d7e3b9b9f..7a519a122 100644
--- a/components/sdk-typescript/src/sandbox_template.ts
+++ b/components/sdk-typescript/src/sandbox_template.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/sandbox_template_api.ts b/components/sdk-typescript/src/sandbox_template_api.ts
index 5fb1b3b45..35bf611ce 100644
--- a/components/sdk-typescript/src/sandbox_template_api.ts
+++ b/components/sdk-typescript/src/sandbox_template_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/secret_source.ts b/components/sdk-typescript/src/secret_source.ts
index ae6ec88ee..13f1bcff3 100644
--- a/components/sdk-typescript/src/secret_source.ts
+++ b/components/sdk-typescript/src/secret_source.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/secret_source_api.ts b/components/sdk-typescript/src/secret_source_api.ts
index 9f6b98fa9..8586227b7 100644
--- a/components/sdk-typescript/src/secret_source_api.ts
+++ b/components/sdk-typescript/src/secret_source_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/sdk-typescript/src/user.ts b/components/sdk-typescript/src/user.ts
index 0b04f9665..e8cdc1305 100644
--- a/components/sdk-typescript/src/user.ts
+++ b/components/sdk-typescript/src/user.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { ObjectReference, ListMeta } from './base.js';
diff --git a/components/sdk-typescript/src/user_api.ts b/components/sdk-typescript/src/user_api.ts
index 5a935147d..8c0c428f6 100644
--- a/components/sdk-typescript/src/user_api.ts
+++ b/components/sdk-typescript/src/user_api.ts
@@ -1,6 +1,6 @@
// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT.
// Source: components/api-server/openapi/openapi.yaml
-// Spec SHA256: b39c23e44eaf38276f86232577bc206efc3fdc7b998da9c074d87f66c7b4f61d
+// Spec SHA256: 4baf5c692945496c1ba8a1c195d838e0f028eb1471cf283b226daf69880cdc1d
import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js';
import { sdkFetch, buildQueryString } from './base.js';
diff --git a/components/web-console/app/features/gateways/gateway-create.stories.tsx b/components/web-console/app/features/gateways/gateway-create.stories.tsx
index a0bf2e653..6acd76685 100644
--- a/components/web-console/app/features/gateways/gateway-create.stories.tsx
+++ b/components/web-console/app/features/gateways/gateway-create.stories.tsx
@@ -1,5 +1,6 @@
import type {
GatewayOperations,
+ GatewayPlacementAvailability,
GatewayRecord,
GatewayUiNavigation,
} from "@openshift-online/hypershell-gateway-management-ui";
@@ -10,7 +11,7 @@ import {
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, within } from "storybook/test";
-const placementAvailability = {
+const placementAvailability: GatewayPlacementAvailability = {
awsPublic: true,
awsVpn: true,
ibmPublic: true,
@@ -18,7 +19,7 @@ const placementAvailability = {
localKind: true,
} as const;
-const localDevelopmentAvailability = {
+const localDevelopmentAvailability: GatewayPlacementAvailability = {
awsPublic: false,
awsVpn: false,
ibmPublic: false,
@@ -49,17 +50,14 @@ const navigation: GatewayUiNavigation = {
function GatewayCreatePreview({
availability = placementAvailability,
}: {
- availability?: typeof placementAvailability;
+ availability?: GatewayPlacementAvailability;
}) {
const operationsForStory = {
...gatewayOperations,
getGatewayPlacementAvailability: () => Promise.resolve(availability),
} as GatewayOperations;
return (
-
+
);
diff --git a/components/web-console/locales/en.json b/components/web-console/locales/en.json
index 98a0f0b6b..c614812e7 100644
--- a/components/web-console/locales/en.json
+++ b/components/web-console/locales/en.json
@@ -1615,14 +1615,14 @@
"defaultMessage": "For gateways that need to reach GitLab and other internal Red Hat services.",
"description": "Description of VPN placement."
},
- "app.page.gatewayProvision.vpnRequiresLabel": {
- "defaultMessage": "Red Hat VPN required",
- "description": "Label shown above the VPN placement guidance."
- },
"app.page.gatewayProvision.vpnRequiresAws": {
"defaultMessage": "VPN placement requires AWS.",
"description": "Explanation shown when IBM Cloud is disabled for VPN."
},
+ "app.page.gatewayProvision.vpnRequiresLabel": {
+ "defaultMessage": "Red Hat VPN required",
+ "description": "Label shown above the VPN placement guidance."
+ },
"app.page.gateways.description": {
"defaultMessage": "HyperShell gateways.",
"description": "Browser metadata description for the gateways page."
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
index 611adde1f..e2cbfaa30 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx
@@ -218,10 +218,11 @@ describe("GatewayCreatePage", () => {
expect(radio("VPN").checked).toBe(false);
});
- it("fails closed when placement availability cannot be loaded", async () => {
- operations.getGatewayPlacementAvailability.mockRejectedValue(
- new Error("unavailable"),
- );
+ it("fails closed and retries when placement availability cannot be loaded", async () => {
+ const user = userEvent.setup();
+ operations.getGatewayPlacementAvailability
+ .mockRejectedValueOnce(new Error("unavailable"))
+ .mockResolvedValue(availablePlacement);
renderPage();
expect(
@@ -231,6 +232,14 @@ describe("GatewayCreatePage", () => {
).toBeTruthy();
expect(radio("Public").disabled).toBe(true);
expect(radio("VPN").disabled).toBe(true);
+
+ await user.click(screen.getByRole("button", { name: "Retry" }));
+
+ await waitFor(() => {
+ expect(radio("Public").disabled).toBe(false);
+ expect(radio("VPN").disabled).toBe(false);
+ });
+ expect(operations.getGatewayPlacementAvailability).toHaveBeenCalledTimes(2);
});
it("uses network-specific availability and explains the backend reason", async () => {
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
index be58c6349..f7ad9e632 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
@@ -1,6 +1,7 @@
import {
ActionGroup,
Alert,
+ AlertActionLink,
Button,
Card,
CardBody,
@@ -587,7 +588,7 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
reason: intl.formatMessage(
placementReasonMessages[
availability.data.ibmReason ??
- "no-eligible-cluster"
+ "no-eligible-cluster"
],
),
})
@@ -617,6 +618,15 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
) : null}
{availability.isError ? (
{
+ void availability.refetch();
+ }}
+ >
+ {intl.formatMessage(messages.retry)}
+
+ }
isInline
title={intl.formatMessage(messages.placementAvailabilityError)}
variant="warning"
diff --git a/scripts/sdk-generator/parser_test.go b/scripts/sdk-generator/parser_test.go
index 52770a5dc..0c2d1a514 100644
--- a/scripts/sdk-generator/parser_test.go
+++ b/scripts/sdk-generator/parser_test.go
@@ -81,6 +81,13 @@ func TestParseSpecProjectsGatewayCreateRequestFromOperationSchema(t *testing.T)
if resource.Name != "Gateway" {
continue
}
+ responseFields := make(map[string]Field, len(resource.Fields))
+ for _, field := range resource.Fields {
+ responseFields[field.Name] = field
+ }
+ if responseFields["generation"].Name == "" || responseFields["observed_generation"].Name == "" {
+ t.Fatal("gateway response fields must retain generation and observed_generation")
+ }
if len(resource.CreateFields) == 0 {
t.Fatal("gateway create fields are missing")
}
diff --git a/specs/web-console/gateway-provision-placement.spec.md b/specs/web-console/gateway-provision-placement.spec.md
index 8bd848ef8..af727b6d8 100644
--- a/specs/web-console/gateway-provision-placement.spec.md
+++ b/specs/web-console/gateway-provision-placement.spec.md
@@ -2,7 +2,7 @@
## Purpose
-Define the placement choices presented while provisioning an OpenShell gateway and the server-side resolution of those choices to a concrete `ManagedCluster`. The provision form SHALL let a user choose exactly one network visibility—VPN or public. Public placement SHALL support AWS and IBM Cloud. VPN placement SHALL support AWS only. The client SHALL submit placement intent rather than a cluster identifier. The backend SHALL randomly select a matching eligible cluster and persist its identifier on the created gateway.
+Define the placement choices presented while provisioning an OpenShell gateway and the server-side resolution of those choices to a concrete `ManagedCluster`. The provision form SHALL let a user choose exactly one network visibility: VPN or public. Public placement SHALL support AWS and IBM Cloud. VPN placement SHALL support AWS only. The client SHALL submit placement intent rather than a cluster identifier. The backend SHALL randomly select a matching eligible cluster and persist its identifier on the created gateway.
## Requirements
From 0ec0c093e951fcc698addcbdeae9f38ea724404a Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Mon, 5 Oct 2026 10:23:01 -0500
Subject: [PATCH 07/12] Fix CI issues
---
.../api-server/pkg/api/openapi_embed_test.go | 4 +-
.../plugins/gateways/cluster_fixture_test.go | 5 --
.../test/authz/authz_integration_test.go | 9 +++
.../web-console/e2e/application-shell.spec.ts | 60 ++++++++++----
deploy/base/control-plane/deployment.yaml | 78 -------------------
.../src/gateways/gateway-create.tsx | 26 ++++---
tests/e2e/e2e-openshell.sh | 10 ++-
tests/e2e/lib.sh | 8 +-
tests/e2e/perf/lib_test.sh | 13 ++--
9 files changed, 92 insertions(+), 121 deletions(-)
delete mode 100644 deploy/base/control-plane/deployment.yaml
diff --git a/components/api-server/pkg/api/openapi_embed_test.go b/components/api-server/pkg/api/openapi_embed_test.go
index 05e6f7a6c..4a43be33c 100644
--- a/components/api-server/pkg/api/openapi_embed_test.go
+++ b/components/api-server/pkg/api/openapi_embed_test.go
@@ -55,8 +55,8 @@ func TestGetOpenAPISpecReturnsCompleteContract(t *testing.T) {
operationIDs[operation.OperationID] = method + " " + path
}
}
- if operationCount != 54 {
- t.Fatalf("embedded operation count = %d, want 54", operationCount)
+ if operationCount != 55 {
+ t.Fatalf("embedded operation count = %d, want 55", operationCount)
}
expectedDeletes := map[string]string{
diff --git a/components/api-server/plugins/gateways/cluster_fixture_test.go b/components/api-server/plugins/gateways/cluster_fixture_test.go
index d0aa87bf1..3663d5ccc 100644
--- a/components/api-server/plugins/gateways/cluster_fixture_test.go
+++ b/components/api-server/plugins/gateways/cluster_fixture_test.go
@@ -10,7 +10,6 @@ import (
"github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
"github.com/openshift-online/hypershell/components/api-server/plugins/managedClusters"
- "github.com/openshift-online/hypershell/components/api-server/test"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/api"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/environments"
)
@@ -69,7 +68,3 @@ func createManualCluster(t *testing.T) string {
}
return cluster.ID
}
-
-func openapiErrorReason(_ *test.Helper, err error) string {
- return test.APIErrorReason(err)
-}
diff --git a/components/api-server/test/authz/authz_integration_test.go b/components/api-server/test/authz/authz_integration_test.go
index ac74e9784..b41539e1d 100644
--- a/components/api-server/test/authz/authz_integration_test.go
+++ b/components/api-server/test/authz/authz_integration_test.go
@@ -17,8 +17,11 @@ import (
pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1"
"github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi"
+ "github.com/openshift-online/hypershell/components/api-server/plugins/roleBindings"
+ "github.com/openshift-online/hypershell/components/api-server/plugins/users"
"github.com/openshift-online/hypershell/components/api-server/test"
"github.com/openshift-online/rh-trex-ai/components/api-server/pkg/api"
+ "github.com/openshift-online/rh-trex-ai/components/api-server/pkg/environments"
)
// These tests pin the authorization of ManagedCluster records and of the
@@ -262,9 +265,15 @@ func createGateway(t *testing.T, client *openapi.APIClient, owner principal, clu
created, err := pb.NewGatewayServiceClient(owner.conn).CreateGateway(context.Background(), &pb.CreateGatewayRequest{
Name: fmt.Sprintf("gw-%s", strings.ToLower(api.NewID())[:10]),
ClusterId: clusterID,
+ ReleaseId: "test-release",
})
Expect(err).NotTo(HaveOccurred(), "create gateway")
id := created.GetGateway().GetMetadata().GetId()
+ user, svcErr := users.Service(&environments.Environment().Services).GetByUsername(context.Background(), owner.name)
+ Expect(svcErr).NotTo(HaveOccurred(), "find gateway owner")
+ Expect(roleBindings.Service(&environments.Environment().Services).CreateGatewayOwnerBinding(
+ context.Background(), user.ID, id,
+ )).To(Succeed(), "bind gateway owner")
return &openapi.Gateway{Id: &id, Namespace: created.GetGateway().GetNamespace()}
}
diff --git a/components/web-console/e2e/application-shell.spec.ts b/components/web-console/e2e/application-shell.spec.ts
index db0c1c9da..cbf664398 100644
--- a/components/web-console/e2e/application-shell.spec.ts
+++ b/components/web-console/e2e/application-shell.spec.ts
@@ -51,6 +51,26 @@ test.beforeEach(async ({ browserName, page }) => {
}
await page.route("**/api/hypershell/v1/gateways**", async (route) => {
+ if (
+ new URL(route.request().url()).pathname.endsWith(
+ "/placement-availability",
+ )
+ ) {
+ await route.fulfill({
+ body: JSON.stringify({
+ aws_public: true,
+ aws_reason: "no-eligible-cluster",
+ aws_vpn: false,
+ ibm_public: false,
+ ibm_reason: "no-eligible-cluster",
+ ibm_vpn: false,
+ local_kind: false,
+ }),
+ contentType: "application/json",
+ status: 200,
+ });
+ return;
+ }
const request = route.request();
if (request.method() === "DELETE") {
gatewayDeleted = true;
@@ -251,11 +271,9 @@ test("operates gateway rows and opens provisioning", async ({ page }) => {
await page.getByRole("link", { name: "Provision gateway" }).click();
await expect(page).toHaveURL(/\/gateways\/new$/);
- // The picker offers registered clusters only; the single registered
- // cluster is preselected.
- await expect(page.getByRole("combobox", { name: "Cluster" })).toHaveValue(
- "Cluster East",
- );
+ await expect(
+ page.getByRole("heading", { level: 1, name: "Provision gateway" }),
+ ).toBeVisible();
await expect(page.getByLabel("Namespace", { exact: true })).toHaveCount(0);
});
@@ -458,6 +476,24 @@ test("provisions a gateway on an existing managed cluster", async ({
page,
}) => {
let requestBody: Record | undefined;
+ await page.route(
+ "**/api/hypershell/v1/gateways/placement-availability",
+ async (route) => {
+ await route.fulfill({
+ contentType: "application/json",
+ status: 200,
+ body: JSON.stringify({
+ aws_public: true,
+ aws_reason: "no-eligible-cluster",
+ aws_vpn: false,
+ ibm_public: false,
+ ibm_reason: "no-eligible-cluster",
+ ibm_vpn: false,
+ local_kind: false,
+ }),
+ });
+ },
+ );
await page.route("**/api/hypershell/v1/gateways", async (route) => {
if (route.request().method() !== "POST") {
await route.continue();
@@ -491,14 +527,10 @@ test("provisions a gateway on an existing managed cluster", async ({
await expect(
page.getByRole("heading", { level: 1, name: "Provision gateway" }),
).toBeFocused();
- const clusterInput = page.getByRole("combobox", { name: "Cluster" });
- // A single registered cluster is preselected; clearing and re-selecting it
- // exercises the search flow.
- await expect(clusterInput).toHaveValue("Cluster East");
- await page.getByRole("button", { name: "Clear cluster search" }).click();
- await clusterInput.fill("East");
- await page.getByText("Cluster East", { exact: true }).click();
- await expect(clusterInput).toHaveValue("Cluster East");
+ await page.getByText("Public", { exact: true }).click({ force: true });
+ await page
+ .getByText("Amazon Web Services", { exact: true })
+ .click({ force: true });
await expect(page.getByText("Provider: AWS; region: us-east-1")).toHaveCount(
0,
);
@@ -529,8 +561,8 @@ test("provisions a gateway on an existing managed cluster", async ({
).toBeVisible();
await expect(page.getByText("Not available")).toHaveCount(1);
expect(requestBody).toEqual({
- cluster_id: "cluster-east",
name: "team-gateway",
+ placement: { network: "public", provider: "aws" },
route: '{"enabled":true}',
});
});
diff --git a/deploy/base/control-plane/deployment.yaml b/deploy/base/control-plane/deployment.yaml
deleted file mode 100644
index 939b62547..000000000
--- a/deploy/base/control-plane/deployment.yaml
+++ /dev/null
@@ -1,78 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: hypershell-control-plane
-spec:
- # Keep replicas at 1. Attention gauges are cluster-local; the BFF dedupes
- # same-cluster replica series via max by (hypershell_cluster_id), but a
- # single replica remains the supported topology for the control plane.
- replicas: 1
- selector:
- matchLabels:
- app: hypershell-control-plane
- template:
- metadata:
- labels:
- app: hypershell-control-plane
- spec:
- serviceAccountName: hypershell-control-plane
- containers:
- - name: control-plane
- image: quay.io/redhat-services-prod/hcm-eng-prod-tenant/hypershell-main/hypershell-control-plane-main:latest
- # HYPERSHELL_MANAGED_CLUSTER_NAME and OIDC_ISSUER / OIDC_CLIENT_ID /
- # OIDC_CLIENT_SECRET are required and must be supplied by the
- # consuming overlay; the control plane exits at startup without them
- # (see deploy/base/controller.yaml and control-plane.spec.md).
- env:
- - name: HYPERSHELL_GRPC_SERVER_ADDR
- value: "hypershell-api-server:9000"
- - name: HYPERSHELL_API_SERVER_URL
- value: "http://hypershell-api-server:8000"
- - name: HYPERSHELL_MANAGED_CLUSTER_PROVIDER
- value: "aws"
- - name: HYPERSHELL_MANAGED_CLUSTER_VISIBILITY
- value: "public"
- - name: HYPERSHELL_NAMESPACE
- value: "hypershell-stage"
- - name: HYPERSHELL_LOG_LEVEL
- value: "info"
- - name: GATEWAY_API_BASE_DOMAIN
- value: "openshell.stage.devshift.net"
- - name: GATEWAY_API_GATEWAY_NAME
- value: "openshell-grpc-gateway"
- - name: GATEWAY_API_GATEWAY_NAMESPACE
- value: "openshift-ingress"
- - name: GATEWAY_API_HTTP_LISTENER_NAME
- value: "grpc"
- # Plaintext in-cluster gRPC provisioner endpoint. A NetworkPolicy
- # restricts port 9443 to the api-server pod; the channel mirrors the
- # sibling control-plane -> api-server watch channel (no mTLS).
- - name: HYPERSHELL_SERVICE_ACCOUNT_PROVISIONER_BIND_ADDRESS
- value: "0.0.0.0:9443"
- - name: GATEWAY_IMAGE
- value: quay.io/opendatahub/odh-openshell-gateway:v0.1.2-rhaiv.7
- - name: GATEWAY_SUPERVISOR_IMAGE
- value: quay.io/opendatahub/odh-openshell-supervisor:v0.1.2-rhaiv.7
- - name: GATEWAY_SANDBOX_RUNTIME_IMAGE
- value: quay.io/opendatahub/odh-openshell-sandbox:v0.1.2-rhaiv.7
- ports:
- - name: http
- containerPort: 8080
- - name: provisioner
- containerPort: 9443
- resources:
- requests:
- cpu: 100m
- memory: 256Mi
- limits:
- cpu: 500m
- memory: 512Mi
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: true
- runAsNonRoot: true
- seccompProfile:
- type: RuntimeDefault
- capabilities:
- drop:
- - ALL
diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
index f7ad9e632..f87d0eefc 100644
--- a/packages/gateway-management-ui/src/gateways/gateway-create.tsx
+++ b/packages/gateway-management-ui/src/gateways/gateway-create.tsx
@@ -45,6 +45,14 @@ const placementReasonMessages = {
"no-eligible-cluster": messages.noEligibleCluster,
} as const;
+function placementReasonKey(
+ ...reasons: readonly (string | null | undefined)[]
+): keyof typeof placementReasonMessages {
+ return (reasons.find(
+ (reason) => reason !== undefined && reason !== null && reason !== "",
+ ) ?? "no-eligible-cluster") as keyof typeof placementReasonMessages;
+}
+
export interface GatewayCreatePageProps {
onCreated?: (gatewayId: string) => Promise | void;
}
@@ -472,9 +480,10 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
network: intl.formatMessage(messages.publicNetwork),
reason: intl.formatMessage(
placementReasonMessages[
- availability.data.awsReason ??
- availability.data.ibmReason ??
- "no-eligible-cluster"
+ placementReasonKey(
+ availability.data.awsReason,
+ availability.data.ibmReason,
+ )
],
),
})
@@ -508,8 +517,7 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
network: intl.formatMessage(messages.vpnNetwork),
reason: intl.formatMessage(
placementReasonMessages[
- availability.data.awsReason ??
- "no-eligible-cluster"
+ placementReasonKey(availability.data.awsReason)
],
),
})
@@ -552,8 +560,7 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
provider: intl.formatMessage(messages.awsProvider),
reason: intl.formatMessage(
placementReasonMessages[
- availability.data.awsReason ??
- "no-eligible-cluster"
+ placementReasonKey(availability.data.awsReason)
],
),
})
@@ -587,8 +594,9 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) {
),
reason: intl.formatMessage(
placementReasonMessages[
- availability.data.ibmReason ??
- "no-eligible-cluster"
+ placementReasonKey(
+ availability.data.ibmReason,
+ )
],
),
})
diff --git a/tests/e2e/e2e-openshell.sh b/tests/e2e/e2e-openshell.sh
index 233e33bff..4bf0a0ac9 100755
--- a/tests/e2e/e2e-openshell.sh
+++ b/tests/e2e/e2e-openshell.sh
@@ -444,7 +444,7 @@ else
fail_test "Could not discover seeded cluster id"
exit 1
fi
- dim " Creating gateway on registered cluster_id=${E2E_CLUSTER_ID}; the gateway database is provisioned by the control plane"
+ dim " Creating gateway with server-side placement; the gateway database is provisioned by the control plane"
show_cmd "api_curl -X POST ${API_HOST}/api/hypershell/v1/gateways -d '{name: ${GW_NAME}, oidc: ...}'"
GW_CREATE_BODY=$(e2e_gateway_create_body "$GW_NAME")
@@ -1832,7 +1832,8 @@ except Exception:
import json, os
body = {
'name': os.environ['GW_NAME'],
- 'cluster_id': os.environ['E2E_CLUSTER_ID'],
+ 'placement': ({'mode': 'local-kind'} if os.environ.get('E2E_INFRA_DRIVER') == 'kind'
+ else {'network': 'public', 'provider': 'aws'}),
'oidc': json.dumps({
'issuer': os.environ['E2E_OIDC_ISSUER'],
'audience': os.environ['E2E_OIDC_CLIENT_ID'],
@@ -2003,7 +2004,8 @@ print('true' if has_owner else 'false')
import json, os
body = {
'name': os.environ['GW_NAME'],
- 'cluster_id': os.environ['E2E_CLUSTER_ID'],
+ 'placement': ({'mode': 'local-kind'} if os.environ.get('E2E_INFRA_DRIVER') == 'kind'
+ else {'network': 'public', 'provider': 'aws'}),
'oidc': json.dumps({
'issuer': os.environ['E2E_OIDC_ISSUER'],
'audience': os.environ['E2E_OIDC_CLIENT_ID'],
@@ -2396,6 +2398,7 @@ import json, os
print(json.dumps({
'name': os.environ['GW_NAME'],
'cluster_id': '',
+ 'placement': {'mode': 'local-kind'} if os.environ.get('E2E_INFRA_DRIVER') == 'kind' else {'network': 'public', 'provider': 'aws'},
'oidc': json.dumps({'issuer': os.environ['E2E_OIDC_ISSUER'], 'audience': os.environ['E2E_OIDC_CLIENT_ID'],
'roles_claim': 'groups', 'admin_role': 'hypershell-admins', 'user_role': 'hypershell-users'}),
'route': json.dumps({'enabled': True}),
@@ -2425,6 +2428,7 @@ import json, os
print(json.dumps({
'name': os.environ['GW_NAME'],
'cluster_id': os.environ['PH_ID'],
+ 'placement': {'mode': 'local-kind'} if os.environ.get('E2E_INFRA_DRIVER') == 'kind' else {'network': 'public', 'provider': 'aws'},
'oidc': json.dumps({'issuer': os.environ['E2E_OIDC_ISSUER'], 'audience': os.environ['E2E_OIDC_CLIENT_ID'],
'roles_claim': 'groups', 'admin_role': 'hypershell-admins', 'user_role': 'hypershell-users'}),
'route': json.dumps({'enabled': True}),
diff --git a/tests/e2e/lib.sh b/tests/e2e/lib.sh
index 24ca9f081..36820398b 100755
--- a/tests/e2e/lib.sh
+++ b/tests/e2e/lib.sh
@@ -796,16 +796,18 @@ print(obj.get('cluster_id', '') or '')
[[ -z "${E2E_CLUSTER_ID:-}" && -n "$cluster" ]] && E2E_CLUSTER_ID="$cluster"
}
-# Print a gateway create body that reuses the seeded cluster id.
+# Print a gateway create body using the deployment's placement intent.
e2e_gateway_create_body() {
local name="${1:?gateway name required}"
GW_NAME="$name" E2E_OIDC_ISSUER="$E2E_OIDC_ISSUER" \
E2E_OIDC_CLIENT_ID="$E2E_OIDC_CLIENT_ID" \
- E2E_CLUSTER_ID="${E2E_CLUSTER_ID}" python3 -c "
+ E2E_INFRA_DRIVER="${E2E_INFRA_DRIVER:-}" python3 -c "
import json, os
+placement = ({'mode': 'local-kind'} if os.environ['E2E_INFRA_DRIVER'] == 'kind'
+ else {'network': 'public', 'provider': 'aws'})
body = {
'name': os.environ['GW_NAME'],
- 'cluster_id': os.environ['E2E_CLUSTER_ID'],
+ 'placement': placement,
'oidc': json.dumps({
'issuer': os.environ['E2E_OIDC_ISSUER'],
'audience': os.environ['E2E_OIDC_CLIENT_ID'],
diff --git a/tests/e2e/perf/lib_test.sh b/tests/e2e/perf/lib_test.sh
index 71be79c47..a7a980b1c 100755
--- a/tests/e2e/perf/lib_test.sh
+++ b/tests/e2e/perf/lib_test.sh
@@ -147,21 +147,20 @@ eval "$_orig_fetch"
unset _orig_discover _orig_fetch
E2E_CLUSTER_ID=""
-# The create body carries the registered cluster id only (no release_id since
-# GatewayRelease has been removed from the data model).
+# The create body carries placement intent without the retired release_id.
E2E_CLUSTER_ID="c-registered" E2E_OIDC_ISSUER=https://example/realms/x E2E_OIDC_CLIENT_ID=cli
body=$(e2e_gateway_create_body gw-unseeded)
-if echo "$body" | grep -q '"cluster_id": "c-registered"' && ! echo "$body" | grep -q '"release_id"'; then
- pass_u "gateway create body sends the registered cluster_id without a release_id"
+if echo "$body" | grep -q '"placement": {"network": "public", "provider": "aws"}' && ! echo "$body" | grep -q '"release_id"'; then
+ pass_u "gateway create body sends placement intent without release_id"
else
- fail_u "gateway create body should send cluster_id without release_id: ${body:0:200}"
+ fail_u "gateway create body should send placement intent without release_id: ${body:0:200}"
fi
E2E_CLUSTER_ID=""
E2E_CLUSTER_ID=c1 E2E_OIDC_ISSUER=https://example/realms/x E2E_OIDC_CLIENT_ID=cli
body=$(e2e_gateway_create_body gw-test)
-if echo "$body" | grep -q '"cluster_id": "c1"' && ! echo "$body" | grep -q 'fleet_id' && ! echo "$body" | grep -q 'database_id'; then
- pass_u "gateway create body omits fleet_id and database_id"
+if echo "$body" | grep -q '"placement": {"network": "public", "provider": "aws"}' && ! echo "$body" | grep -q 'cluster_id' && ! echo "$body" | grep -q 'fleet_id' && ! echo "$body" | grep -q 'database_id'; then
+ pass_u "gateway create body uses placement and omits cluster/fleet/database ids"
else
fail_u "gateway create body unexpected: ${body:0:200}"
fi
From e9f7832d4559f0196d07856e7c73ada864b56c54 Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 7 Oct 2026 12:30:36 -0500
Subject: [PATCH 08/12] test(e2e): select gateway placement choices
---
tests/e2e/e2e-console.sh | 56 ++++++++++++++--------------------------
1 file changed, 20 insertions(+), 36 deletions(-)
diff --git a/tests/e2e/e2e-console.sh b/tests/e2e/e2e-console.sh
index 704e868e4..d5206622e 100755
--- a/tests/e2e/e2e-console.sh
+++ b/tests/e2e/e2e-console.sh
@@ -383,18 +383,15 @@ else
exit 1
fi
-CLUSTER_OPTION_RE="^$(ab_regex_escape "$SEED_CLUSTER_NAME")( |$)"
-TOGGLE_REF=$(ab_ref button '^Select a cluster$' || true)
-OPTION_REF=""
-if [[ -n "$TOGGLE_REF" ]]; then
- show_cmd "agent-browser click ${TOGGLE_REF} # Select a cluster"
- ab_ok click "$TOGGLE_REF" || true
- OPTION_REF=$(ab_wait_ref option "$CLUSTER_OPTION_RE" 15 || true)
+PUBLIC_REF=$(ab_wait_ref radio '^Public$' 15 || true)
+AWS_REF=""
+if [[ -n "$PUBLIC_REF" ]] && show_cmd "agent-browser click ${PUBLIC_REF} # Public" && ab_ok click "$PUBLIC_REF"; then
+ AWS_REF=$(ab_wait_ref radio '^Amazon Web Services$' 15 || true)
fi
-if [[ -n "$OPTION_REF" ]] && show_cmd "agent-browser click ${OPTION_REF} # ${SEED_CLUSTER_NAME}" && ab_ok click "$OPTION_REF"; then
- pass "Selected cluster ${SEED_CLUSTER_NAME} in the Cluster typeahead"
+if [[ -n "$AWS_REF" ]] && show_cmd "agent-browser click ${AWS_REF} # Amazon Web Services" && ab_ok click "$AWS_REF"; then
+ pass "Selected Public placement on Amazon Web Services"
else
- ab_fail "Cluster option ${SEED_CLUSTER_NAME} not found in the Cluster typeahead"
+ ab_fail "Public/Amazon Web Services placement was not available"
exit 1
fi
@@ -737,44 +734,31 @@ if [[ -n "$WORKSPACE" ]] && e2e_step long; then
fi
sep
-# ── 7. Cluster typeahead + CLI install docs ──────────────────────────────
-# CON-E2E-14: the gateway create form lists every registered ManagedCluster as a
-# selectable target, and the connection tab links to the external OpenShell CLI
-# install docs (opens in a new tab with a safe rel).
+# ── 7. Placement choices + CLI install docs ───────────────────────────────
+# The gateway create form exposes server-side placement choices, and the
+# connection tab links to the external OpenShell CLI install docs.
echo ""
-e2e_area "7. Cluster Typeahead and CLI Install Docs"
+e2e_area "7. Placement Choices and CLI Install Docs"
echo ""
ab_use "$ADMIN_SESSION"
-# Cluster typeahead: the seeded cluster is listed and selectable. (Area 3 already
-# selects it to provision; here we assert the listing explicitly, and both
-# clusters under E2E_MULTICLUSTER.)
+# Placement choices: the managed public/AWS choice is listed and selectable.
if ab_open "${CONSOLE_HOST}/gateways/new" \
&& ab_wait_ref textbox "$NAME_TEXTBOX" "$PAGE_TIMEOUT_S" >/dev/null; then
- TA_TOGGLE=$(ab_ref button '^Select a cluster$' || true)
- TA_OPTION=""
- if [[ -n "$TA_TOGGLE" ]] && ab_ok click "$TA_TOGGLE"; then
- TA_OPTION=$(ab_wait_ref option "^$(ab_regex_escape "$SEED_CLUSTER_NAME")( |\$)" 15 || true)
+ TA_PUBLIC=$(ab_wait_ref radio '^Public$' 15 || true)
+ TA_AWS=""
+ if [[ -n "$TA_PUBLIC" ]] && ab_ok click "$TA_PUBLIC"; then
+ TA_AWS=$(ab_wait_ref radio '^Amazon Web Services$' 15 || true)
fi
- if [[ -n "$TA_OPTION" ]]; then
- pass "Cluster typeahead lists the registered cluster ${SEED_CLUSTER_NAME} as selectable"
+ if [[ -n "$TA_AWS" ]]; then
+ pass "Placement choices include Public on Amazon Web Services"
else
- ab_fail "Cluster typeahead did not list ${SEED_CLUSTER_NAME}"
- fi
- if e2e_truthy "${E2E_MULTICLUSTER}" && [[ -n "${E2E_SEED_CLUSTER_NAME_2:-}" ]]; then
- TA_OPTION2=$(ab_wait_ref option "^$(ab_regex_escape "$E2E_SEED_CLUSTER_NAME_2")( |\$)" 15 || true)
- if [[ -n "$TA_OPTION2" ]]; then
- pass "Cluster typeahead also lists the second cluster ${E2E_SEED_CLUSTER_NAME_2} (E2E_MULTICLUSTER)"
- else
- ab_fail "Cluster typeahead did not list the second cluster ${E2E_SEED_CLUSTER_NAME_2}"
- fi
- else
- dim " Second-cluster typeahead check skipped (E2E_MULTICLUSTER unset)"
+ ab_fail "Public/Amazon Web Services placement was not available"
fi
else
- ab_fail "Could not open the gateway create form for the cluster typeahead check"
+ ab_fail "Could not open the gateway create form for the placement check"
fi
# Install-docs link on the gateway detail (connection) view. It renders once the
From 1efe6d5ab1ba70edbd575045c42940d4df4f268f Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 7 Oct 2026 12:31:12 -0500
Subject: [PATCH 09/12] linting after rebase
---
.../app/adapters/api/gateway-operations.test.ts | 8 ++++----
.../web-console/app/adapters/api/gateway-operations.ts | 8 ++++----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/components/web-console/app/adapters/api/gateway-operations.test.ts b/components/web-console/app/adapters/api/gateway-operations.test.ts
index 45c97d082..a64bf9c46 100644
--- a/components/web-console/app/adapters/api/gateway-operations.test.ts
+++ b/components/web-console/app/adapters/api/gateway-operations.test.ts
@@ -680,10 +680,10 @@ describe("gateway API operations adapter", () => {
);
expect(gatewayApi.create).toHaveBeenCalledWith(
- {
- name: "team-gateway",
- placement: { network: "public", provider: "aws" },
- route: '{"enabled":true}',
+ {
+ name: "team-gateway",
+ placement: { network: "public", provider: "aws" },
+ route: '{"enabled":true}',
},
{ signal: undefined },
);
diff --git a/components/web-console/app/adapters/api/gateway-operations.ts b/components/web-console/app/adapters/api/gateway-operations.ts
index d047cb9ac..48c78ba85 100644
--- a/components/web-console/app/adapters/api/gateway-operations.ts
+++ b/components/web-console/app/adapters/api/gateway-operations.ts
@@ -572,10 +572,10 @@ export function createGatewayControlPlaneAdapter(
return mapFailure(async () =>
toGatewayRecord(
await apiClient(apiFactory, context).gateways.create(
- {
- name: input.name,
- placement,
- route: JSON.stringify({ enabled: true }),
+ {
+ name: input.name,
+ placement,
+ route: JSON.stringify({ enabled: true }),
},
{ signal: context.signal },
),
From 54785e2984b23defdccbcf246aa4decaa234fa5e Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 7 Oct 2026 12:31:51 -0500
Subject: [PATCH 10/12] test(e2e): avoid sticky header placement clicks
---
tests/e2e/e2e-console.sh | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/tests/e2e/e2e-console.sh b/tests/e2e/e2e-console.sh
index d5206622e..976dd5218 100755
--- a/tests/e2e/e2e-console.sh
+++ b/tests/e2e/e2e-console.sh
@@ -385,10 +385,18 @@ fi
PUBLIC_REF=$(ab_wait_ref radio '^Public$' 15 || true)
AWS_REF=""
-if [[ -n "$PUBLIC_REF" ]] && show_cmd "agent-browser click ${PUBLIC_REF} # Public" && ab_ok click "$PUBLIC_REF"; then
+if [[ -n "$PUBLIC_REF" ]] \
+ && ab_ok scrollintoview "$PUBLIC_REF" \
+ && ab_ok scroll down 240 \
+ && show_cmd "agent-browser click ${PUBLIC_REF} # Public" \
+ && ab_ok click "$PUBLIC_REF"; then
AWS_REF=$(ab_wait_ref radio '^Amazon Web Services$' 15 || true)
fi
-if [[ -n "$AWS_REF" ]] && show_cmd "agent-browser click ${AWS_REF} # Amazon Web Services" && ab_ok click "$AWS_REF"; then
+if [[ -n "$AWS_REF" ]] \
+ && ab_ok scrollintoview "$AWS_REF" \
+ && ab_ok scroll down 240 \
+ && show_cmd "agent-browser click ${AWS_REF} # Amazon Web Services" \
+ && ab_ok click "$AWS_REF"; then
pass "Selected Public placement on Amazon Web Services"
else
ab_fail "Public/Amazon Web Services placement was not available"
@@ -749,10 +757,16 @@ if ab_open "${CONSOLE_HOST}/gateways/new" \
&& ab_wait_ref textbox "$NAME_TEXTBOX" "$PAGE_TIMEOUT_S" >/dev/null; then
TA_PUBLIC=$(ab_wait_ref radio '^Public$' 15 || true)
TA_AWS=""
- if [[ -n "$TA_PUBLIC" ]] && ab_ok click "$TA_PUBLIC"; then
+ if [[ -n "$TA_PUBLIC" ]] \
+ && ab_ok scrollintoview "$TA_PUBLIC" \
+ && ab_ok scroll down 240 \
+ && ab_ok click "$TA_PUBLIC"; then
TA_AWS=$(ab_wait_ref radio '^Amazon Web Services$' 15 || true)
fi
- if [[ -n "$TA_AWS" ]]; then
+ if [[ -n "$TA_AWS" ]] \
+ && ab_ok scrollintoview "$TA_AWS" \
+ && ab_ok scroll down 240 \
+ && ab_ok click "$TA_AWS"; then
pass "Placement choices include Public on Amazon Web Services"
else
ab_fail "Public/Amazon Web Services placement was not available"
From 23dcf8d43a5cec8b3d32956c50d6c83b36eaf963 Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 7 Oct 2026 12:34:15 -0500
Subject: [PATCH 11/12] test(e2e): use local kind placement in kind console
runs
---
tests/e2e/e2e-console.sh | 43 +++++++++++++---------------------------
1 file changed, 14 insertions(+), 29 deletions(-)
diff --git a/tests/e2e/e2e-console.sh b/tests/e2e/e2e-console.sh
index 976dd5218..b7c48caa9 100755
--- a/tests/e2e/e2e-console.sh
+++ b/tests/e2e/e2e-console.sh
@@ -383,23 +383,15 @@ else
exit 1
fi
-PUBLIC_REF=$(ab_wait_ref radio '^Public$' 15 || true)
-AWS_REF=""
-if [[ -n "$PUBLIC_REF" ]] \
- && ab_ok scrollintoview "$PUBLIC_REF" \
+LOCAL_KIND_REF=$(ab_wait_ref radio '^Use local-kind$' 15 || true)
+if [[ -n "$LOCAL_KIND_REF" ]] \
+ && ab_ok scrollintoview "$LOCAL_KIND_REF" \
&& ab_ok scroll down 240 \
- && show_cmd "agent-browser click ${PUBLIC_REF} # Public" \
- && ab_ok click "$PUBLIC_REF"; then
- AWS_REF=$(ab_wait_ref radio '^Amazon Web Services$' 15 || true)
-fi
-if [[ -n "$AWS_REF" ]] \
- && ab_ok scrollintoview "$AWS_REF" \
- && ab_ok scroll down 240 \
- && show_cmd "agent-browser click ${AWS_REF} # Amazon Web Services" \
- && ab_ok click "$AWS_REF"; then
- pass "Selected Public placement on Amazon Web Services"
+ && show_cmd "agent-browser click ${LOCAL_KIND_REF} # Use local-kind" \
+ && ab_ok click "$LOCAL_KIND_REF"; then
+ pass "Selected local-kind placement"
else
- ab_fail "Public/Amazon Web Services placement was not available"
+ ab_fail "Use local-kind placement was not available"
exit 1
fi
@@ -752,24 +744,17 @@ echo ""
ab_use "$ADMIN_SESSION"
-# Placement choices: the managed public/AWS choice is listed and selectable.
+# Placement choices: the Kind-local placement is listed and selectable.
if ab_open "${CONSOLE_HOST}/gateways/new" \
&& ab_wait_ref textbox "$NAME_TEXTBOX" "$PAGE_TIMEOUT_S" >/dev/null; then
- TA_PUBLIC=$(ab_wait_ref radio '^Public$' 15 || true)
- TA_AWS=""
- if [[ -n "$TA_PUBLIC" ]] \
- && ab_ok scrollintoview "$TA_PUBLIC" \
- && ab_ok scroll down 240 \
- && ab_ok click "$TA_PUBLIC"; then
- TA_AWS=$(ab_wait_ref radio '^Amazon Web Services$' 15 || true)
- fi
- if [[ -n "$TA_AWS" ]] \
- && ab_ok scrollintoview "$TA_AWS" \
+ TA_LOCAL_KIND=$(ab_wait_ref radio '^Use local-kind$' 15 || true)
+ if [[ -n "$TA_LOCAL_KIND" ]] \
+ && ab_ok scrollintoview "$TA_LOCAL_KIND" \
&& ab_ok scroll down 240 \
- && ab_ok click "$TA_AWS"; then
- pass "Placement choices include Public on Amazon Web Services"
+ && ab_ok click "$TA_LOCAL_KIND"; then
+ pass "Placement choices include local-kind"
else
- ab_fail "Public/Amazon Web Services placement was not available"
+ ab_fail "Use local-kind placement was not available"
fi
else
ab_fail "Could not open the gateway create form for the placement check"
From 8345582fdae42337d3444f2c58bc04b84ba6467a Mon Sep 17 00:00:00 2001
From: Kim Doberstein
Date: Wed, 7 Oct 2026 13:01:07 -0500
Subject: [PATCH 12/12] fix(gateway): use versioned placement availability
endpoint
---
.../cli/data/generated/tui/descriptor.json | 144 +++++++++++++++++-
components/sdk-go/client/gateway_api.go | 2 +-
components/sdk-typescript/src/gateway_api.ts | 2 +-
.../sdk-generator/templates/go/client.go.tmpl | 2 +-
.../sdk-generator/templates/ts/client.ts.tmpl | 2 +-
tests/e2e/e2e-console.sh | 22 +--
6 files changed, 153 insertions(+), 21 deletions(-)
diff --git a/components/cli/data/generated/tui/descriptor.json b/components/cli/data/generated/tui/descriptor.json
index f675de985..091ec418a 100644
--- a/components/cli/data/generated/tui/descriptor.json
+++ b/components/cli/data/generated/tui/descriptor.json
@@ -545,6 +545,65 @@
"listOperationId": "listSecretSources",
"source": {}
},
+ {
+ "id": "collection:/api/hypershell/v1/gateways/placement-availability:#/schemas/GatewayPlacementAvailability",
+ "kind": "collection",
+ "schemaRef": "#/schemas/GatewayPlacementAvailability",
+ "label": "GatewayPlacementAvailability",
+ "defaultSort": "aws_public",
+ "columns": [
+ {
+ "property": "aws_public",
+ "label": "AWS PUBLIC",
+ "priority": 0,
+ "type": "boolean"
+ },
+ {
+ "property": "aws_reason",
+ "label": "AWS REASON",
+ "priority": 0,
+ "type": "string"
+ },
+ {
+ "property": "aws_vpn",
+ "label": "AWS VPN",
+ "priority": 0,
+ "type": "boolean"
+ },
+ {
+ "property": "ibm_public",
+ "label": "IBM PUBLIC",
+ "priority": 0,
+ "type": "boolean"
+ },
+ {
+ "property": "ibm_reason",
+ "label": "IBM REASON",
+ "priority": 0,
+ "type": "string"
+ },
+ {
+ "property": "ibm_vpn",
+ "label": "IBM VPN",
+ "priority": 0,
+ "type": "boolean"
+ },
+ {
+ "property": "local_kind",
+ "label": "LOCAL KIND",
+ "priority": 0,
+ "type": "boolean"
+ }
+ ],
+ "operationIds": [
+ "getGatewayPlacementAvailability"
+ ],
+ "capabilities": [
+ "get"
+ ],
+ "getOperationId": "getGatewayPlacementAvailability",
+ "source": {}
+ },
{
"id": "collection:/api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id}/revoke:#/schemas/OpenShellGatewayServiceAccountListItem",
"kind": "collection",
@@ -1062,6 +1121,12 @@
"priority": 0,
"type": "string",
"format": "date-time"
+ },
+ {
+ "property": "visibility",
+ "label": "VISIBILITY",
+ "priority": 0,
+ "type": "string"
}
],
"operationIds": [
@@ -2264,6 +2329,12 @@
"priority": 0,
"type": "string",
"format": "date-time"
+ },
+ {
+ "property": "visibility",
+ "label": "VISIBILITY",
+ "priority": 0,
+ "type": "string"
}
],
"operationIds": [
@@ -2638,11 +2709,6 @@
"required": true,
"contentType": "application/json",
"fields": [
- {
- "name": "cluster_id",
- "type": "string",
- "required": true
- },
{
"name": "credential_driver",
"type": "string",
@@ -2676,6 +2742,12 @@
"type": "string",
"required": false
},
+ {
+ "name": "placement",
+ "type": "object",
+ "required": true,
+ "description": "Exactly one of network/provider or mode must be supplied."
+ },
{
"name": "route",
"type": "string",
@@ -2974,7 +3046,8 @@
{
"name": "provider",
"type": "string",
- "required": true
+ "required": true,
+ "description": "Cloud provider capability used for gateway placement."
},
{
"name": "region",
@@ -4195,6 +4268,44 @@
"summary": "Get an gateway by id",
"presentation": {}
},
+ {
+ "id": "getGatewayPlacementAvailability",
+ "method": "GET",
+ "pathParts": [
+ {
+ "literal": "/api/hypershell/v1/gateways/placement-availability"
+ }
+ ],
+ "response": {
+ "contentType": "application/json"
+ },
+ "successStatuses": [
+ "200"
+ ],
+ "servers": [
+ {
+ "url": "http://localhost:8000"
+ },
+ {
+ "url": "/"
+ }
+ ],
+ "security": {
+ "none": false,
+ "requirements": [
+ {
+ "schemes": [
+ "Bearer"
+ ]
+ }
+ ]
+ },
+ "capabilities": [
+ "get"
+ ],
+ "summary": "Returns current provider placement eligibility",
+ "presentation": {}
+ },
{
"id": "getGatewayServiceAccount",
"method": "GET",
@@ -5889,6 +6000,27 @@
"type": "string",
"required": true,
"description": "Human-readable spoke name, unique per fleet (e.g. hyp0-mc1). Must match on every subsequent call."
+ },
+ {
+ "name": "provider",
+ "type": "string",
+ "required": false,
+ "description": "Cloud provider hosting this control plane.",
+ "enum": [
+ "aws",
+ "ibm",
+ "kind"
+ ]
+ },
+ {
+ "name": "visibility",
+ "type": "string",
+ "required": false,
+ "description": "Network visibility supported for gateways on this control plane.",
+ "enum": [
+ "public",
+ "vpn"
+ ]
}
]
},
diff --git a/components/sdk-go/client/gateway_api.go b/components/sdk-go/client/gateway_api.go
index 810fc2f8d..f78e2ff11 100644
--- a/components/sdk-go/client/gateway_api.go
+++ b/components/sdk-go/client/gateway_api.go
@@ -24,7 +24,7 @@ func (c *Client) Gateways() *GatewayAPI {
func (a *GatewayAPI) PlacementAvailability(ctx context.Context) (*types.GatewayPlacementAvailability, error) {
var result types.GatewayPlacementAvailability
- if err := a.client.do(ctx, http.MethodGet, "/gateways/placement-availability", nil, http.StatusOK, &result); err != nil {
+ if err := a.client.do(ctx, http.MethodGet, "/api/hypershell/v1/gateways/placement-availability", nil, http.StatusOK, &result); err != nil {
return nil, err
}
return &result, nil
diff --git a/components/sdk-typescript/src/gateway_api.ts b/components/sdk-typescript/src/gateway_api.ts
index b640509eb..c5ef3d068 100644
--- a/components/sdk-typescript/src/gateway_api.ts
+++ b/components/sdk-typescript/src/gateway_api.ts
@@ -10,7 +10,7 @@ export class GatewayAPI {
constructor(private readonly config: SDKClientConfig) {}
async placementAvailability(opts?: RequestOptions): Promise {
- return sdkFetch(this.config, 'GET', '/gateways/placement-availability', undefined, opts);
+ return sdkFetch(this.config, 'GET', '/api/hypershell/v1/gateways/placement-availability', undefined, opts);
}
async create(data: GatewayCreateRequest, opts?: RequestOptions): Promise {
diff --git a/scripts/sdk-generator/templates/go/client.go.tmpl b/scripts/sdk-generator/templates/go/client.go.tmpl
index ccc82e70b..e0204391f 100644
--- a/scripts/sdk-generator/templates/go/client.go.tmpl
+++ b/scripts/sdk-generator/templates/go/client.go.tmpl
@@ -25,7 +25,7 @@ func (c *Client) {{.Resource.Plural}}() *{{.Resource.Name}}API {
{{if eq .Resource.Name "Gateway"}}
func (a *GatewayAPI) PlacementAvailability(ctx context.Context) (*types.GatewayPlacementAvailability, error) {
var result types.GatewayPlacementAvailability
- if err := a.client.do(ctx, http.MethodGet, "/gateways/placement-availability", nil, http.StatusOK, &result); err != nil {
+ if err := a.client.do(ctx, http.MethodGet, "{{.Resource.AbsoluteCollectionPath}}/placement-availability", nil, http.StatusOK, &result); err != nil {
return nil, err
}
return &result, nil
diff --git a/scripts/sdk-generator/templates/ts/client.ts.tmpl b/scripts/sdk-generator/templates/ts/client.ts.tmpl
index d8d5eb738..6a07383c5 100644
--- a/scripts/sdk-generator/templates/ts/client.ts.tmpl
+++ b/scripts/sdk-generator/templates/ts/client.ts.tmpl
@@ -11,7 +11,7 @@ export class {{.Resource.Name}}API {
constructor(private readonly config: SDKClientConfig) {}
{{if eq .Resource.Name "Gateway"}}
async placementAvailability(opts?: RequestOptions): Promise {
- return sdkFetch(this.config, 'GET', '/gateways/placement-availability', undefined, opts);
+ return sdkFetch(this.config, 'GET', '{{.Resource.AbsoluteCollectionPath}}/placement-availability', undefined, opts);
}
{{end}}
async create(data: {{.Resource.Name}}CreateRequest, opts?: RequestOptions): Promise<{{.Resource.Name}}> {
diff --git a/tests/e2e/e2e-console.sh b/tests/e2e/e2e-console.sh
index b7c48caa9..306a6aacd 100755
--- a/tests/e2e/e2e-console.sh
+++ b/tests/e2e/e2e-console.sh
@@ -384,14 +384,15 @@ else
fi
LOCAL_KIND_REF=$(ab_wait_ref radio '^Use local-kind$' 15 || true)
-if [[ -n "$LOCAL_KIND_REF" ]] \
- && ab_ok scrollintoview "$LOCAL_KIND_REF" \
- && ab_ok scroll down 240 \
- && show_cmd "agent-browser click ${LOCAL_KIND_REF} # Use local-kind" \
- && ab_ok click "$LOCAL_KIND_REF"; then
+if [[ -z "$LOCAL_KIND_REF" ]]; then
+ ab_fail "Use local-kind radio was not found in the provisioning form"
+ exit 1
+fi
+if show_cmd "agent-browser eval local-kind radio click" \
+ && ab_ok eval 'document.querySelector("input[value=local-kind]").click()'; then
pass "Selected local-kind placement"
else
- ab_fail "Use local-kind placement was not available"
+ ab_fail "Could not activate the Use local-kind radio"
exit 1
fi
@@ -748,13 +749,12 @@ ab_use "$ADMIN_SESSION"
if ab_open "${CONSOLE_HOST}/gateways/new" \
&& ab_wait_ref textbox "$NAME_TEXTBOX" "$PAGE_TIMEOUT_S" >/dev/null; then
TA_LOCAL_KIND=$(ab_wait_ref radio '^Use local-kind$' 15 || true)
- if [[ -n "$TA_LOCAL_KIND" ]] \
- && ab_ok scrollintoview "$TA_LOCAL_KIND" \
- && ab_ok scroll down 240 \
- && ab_ok click "$TA_LOCAL_KIND"; then
+ if [[ -z "$TA_LOCAL_KIND" ]]; then
+ ab_fail "Use local-kind radio was not found in the placement form"
+ elif ab_ok eval 'document.querySelector("input[value=local-kind]").click()'; then
pass "Placement choices include local-kind"
else
- ab_fail "Use local-kind placement was not available"
+ ab_fail "Could not activate the Use local-kind radio"
fi
else
ab_fail "Could not open the gateway create form for the placement check"