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} +

+ +

- - - + setProvider("aws")} + /> + setProvider("ibm")} + /> + + + ) : null} +

+ A matching managed cluster is selected at random for the chosen + network and provider. +

+ + + + + + ); } 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 ? ( + + ) : 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 ? ( + + ) : 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."}