|
| 1 | +# Integration Visibility Configuration |
| 2 | + |
| 3 | +**Status:** Final |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Developer Sandbox integrates with several downstream services (e.g. OpenShift Lightspeed, DevSpaces, Workato, etc.). When one of these downstream integrations experiences issues, there is currently no way to hide it from customers without a code change and redeployment. |
| 8 | + |
| 9 | +This design adds a `disabledIntegrations` field to `ToolchainConfig` so that operators can dynamically hide broken integrations from users. The registration service exposes this list via the existing `/api/v1/uiconfig` endpoint, allowing the UI to conditionally hide integration entry points at runtime. |
| 10 | + |
| 11 | +The field uses a **denylist** approach: the normal state is an empty field (all integrations enabled), and admins only add the 1-2 broken integrations when something goes wrong. This avoids the operational burden of maintaining a full allowlist of every integration. |
| 12 | + |
| 13 | +## Design Principles |
| 14 | + |
| 15 | +- **Minimal API surface change**: add a single new field to the existing `ToolchainConfig` CRD rather than introducing a new CRD. |
| 16 | +- **Safe defaults**: when the field is not set, all integrations are enabled (backward compatible with existing deployments). |
| 17 | +- **Operational simplicity**: the common case (everything healthy) requires zero configuration; only broken integrations are listed. |
| 18 | +- **Consistency with existing patterns**: follows the same conventions used by `UICanaryDeploymentWeight` and `WorkatoWebHookURL` for CRD placement and REST exposure. |
| 19 | +- **Decoupled from feature toggles**: feature toggles control per-Space probabilistic template rendering; this mechanism controls UI-level visibility of integrations for all users. |
| 20 | + |
| 21 | +## Architecture / How It Works |
| 22 | + |
| 23 | +``` |
| 24 | +┌──────────────┐ ┌──────────────────┐ ┌─────────────────────┐ |
| 25 | +│ Cluster │ │ Registration │ │ Sandbox UI │ |
| 26 | +│ Admin │──────▶│ Service │──────▶│ (Browser) │ |
| 27 | +│ │ │ │ │ │ |
| 28 | +│ applies │ │ GET /api/v1/ │ │ fetches uiconfig, │ |
| 29 | +│ ToolchainCfg │ │ uiconfig │ │ reads disabled- │ |
| 30 | +└──────────────┘ │ (JWT-secured) │ │ Integrations, │ |
| 31 | + │ └──────────────────┘ │ hides those │ |
| 32 | + │ ▲ └─────────────────────┘ |
| 33 | + ▼ │ |
| 34 | +┌──────────────────────────┐ │ |
| 35 | +│ToolchainConfig CR │ │ |
| 36 | +│ spec.host │ │ |
| 37 | +│ .registrationService │ │ |
| 38 | +│ .disabledIntegrations:│ │ |
| 39 | +│ - "openshift" │────┘ |
| 40 | +└──────────────────────────┘ |
| 41 | + (loaded via commonconfig.LoadLatest) |
| 42 | +``` |
| 43 | + |
| 44 | +1. **Cluster admin** updates the `ToolchainConfig` CR, adding broken integrations to `disabledIntegrations`. |
| 45 | +2. **Registration service** picks up the change via its cached client (same mechanism used for all other config — `commonconfig.LoadLatest`). |
| 46 | +3. **`GET /api/v1/uiconfig`** (JWT-secured) serves the disabled list alongside existing fields (`uiCanaryDeploymentWeight`, `workatoWebHookURL`). |
| 47 | +4. **UI** fetches the endpoint and hides any integration present in the `disabledIntegrations` array. |
| 48 | + |
| 49 | +## Core Concepts |
| 50 | + |
| 51 | +### Integration identifier |
| 52 | + |
| 53 | +Each integration is represented by a value of the `IntegrationName` type, a named string with kubebuilder enum validation. The valid identifiers are defined as Go constants in the API module: |
| 54 | + |
| 55 | +| Constant | Value | |
| 56 | +|----------|-------| |
| 57 | +| `IntegrationOpenShift` | `"openshift"` | |
| 58 | +| `IntegrationOpenShiftAI` | `"openshift-ai"` | |
| 59 | +| `IntegrationDevSpaces` | `"devspaces"` | |
| 60 | +| `IntegrationAnsibleAutomationPlatform` | `"ansible-automation-platform"` | |
| 61 | +| `IntegrationOpenShiftVirtualization` | `"openshift-virtualization"` | |
| 62 | + |
| 63 | +Kubernetes rejects any value not in this enum at admission time, preventing typos and misconfiguration. Adding a new integration requires an API module change and CRD regeneration. |
| 64 | + |
| 65 | +### Default behavior |
| 66 | + |
| 67 | +When `disabledIntegrations` is nil or empty, **all integrations are enabled**. This preserves backward compatibility — existing deployments without the field continue to work with everything visible. Admins only populate the field when they need to hide something. |
| 68 | + |
| 69 | +### Field location |
| 70 | + |
| 71 | +The `disabledIntegrations` field lives under `spec.host.registrationService` in the `ToolchainConfig` CRD, co-located with other UI-facing configuration (`uiCanaryDeploymentWeight`, `workatoWebHookURL`). |
| 72 | + |
| 73 | +### CRD field definition |
| 74 | + |
| 75 | +```go |
| 76 | +// In api/v1alpha1/toolchainconfig_types.go: |
| 77 | + |
| 78 | +// +kubebuilder:validation:Enum=openshift;openshift-ai;devspaces;ansible-automation-platform;openshift-virtualization |
| 79 | +type IntegrationName string |
| 80 | + |
| 81 | +const ( |
| 82 | + IntegrationOpenShift IntegrationName = "openshift" |
| 83 | + IntegrationOpenShiftAI IntegrationName = "openshift-ai" |
| 84 | + IntegrationDevSpaces IntegrationName = "devspaces" |
| 85 | + IntegrationAnsibleAutomationPlatform IntegrationName = "ansible-automation-platform" |
| 86 | + IntegrationOpenShiftVirtualization IntegrationName = "openshift-virtualization" |
| 87 | +) |
| 88 | + |
| 89 | +// In RegistrationServiceConfig: |
| 90 | + |
| 91 | +// DisabledIntegrations specifies the list of integrations that should be |
| 92 | +// hidden/disabled in the UI. When nil or empty, all integrations are |
| 93 | +// considered enabled. Only listed integrations are hidden. |
| 94 | +// +optional |
| 95 | +// +listType=set |
| 96 | +DisabledIntegrations []IntegrationName `json:"disabledIntegrations,omitempty"` |
| 97 | +``` |
| 98 | + |
| 99 | +### REST response |
| 100 | + |
| 101 | +The existing `UIConfigResponse` in the registration service is extended with a `disabledIntegrations` field that mirrors the CRD 1:1: |
| 102 | + |
| 103 | +```go |
| 104 | +type UIConfigResponse struct { |
| 105 | + UICanaryDeploymentWeight int `json:"uiCanaryDeploymentWeight"` |
| 106 | + WorkatoWebHookURL string `json:"workatoWebHookURL"` |
| 107 | + DisabledIntegrations []toolchainv1alpha1.IntegrationName `json:"disabledIntegrations"` |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +Example JSON response from `GET /api/v1/uiconfig` when OpenShift is down: |
| 112 | + |
| 113 | +```json |
| 114 | +{ |
| 115 | + "uiCanaryDeploymentWeight": 20, |
| 116 | + "workatoWebHookURL": "https://...", |
| 117 | + "disabledIntegrations": ["openshift"] |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +Example when everything is healthy (field not set in CR): |
| 122 | + |
| 123 | +```json |
| 124 | +{ |
| 125 | + "uiCanaryDeploymentWeight": 20, |
| 126 | + "workatoWebHookURL": "https://...", |
| 127 | + "disabledIntegrations": [] |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +The handler normalizes nil to an empty slice before serialization, so the response always contains `"disabledIntegrations": []` (never absent or `null`). The UI checks `disabledIntegrations.includes("x")` before rendering each integration entry point. |
| 132 | + |
| 133 | +## Implementation Plan |
| 134 | + |
| 135 | +Changes span **three repositories** in the codeready-toolchain ecosystem: |
| 136 | + |
| 137 | +### Phase 1: API types (`codeready-toolchain/api`) |
| 138 | + |
| 139 | +1. Define the `IntegrationName` type with a `+kubebuilder:validation:Enum` marker and the associated constants in `api/v1alpha1/toolchainconfig_types.go`. |
| 140 | +2. Add `DisabledIntegrations []IntegrationName` to `RegistrationServiceConfig` with `+listType=set` and `+optional` markers. |
| 141 | +3. Run code generation (`make generate`) to update DeepCopy methods and CRD manifests. |
| 142 | + |
| 143 | +### Phase 2: Host operator (`codeready-toolchain/host-operator`) |
| 144 | + |
| 145 | +1. Bump the `codeready-toolchain/api` dependency in `go.mod` to pick up the new field, then run `make generate` to regenerate the CRD YAML in `config/crd/bases/`. |
| 146 | + |
| 147 | +### Phase 3: Registration service (`codeready-toolchain/registration-service`) |
| 148 | + |
| 149 | +1. Add a `DisabledIntegrations() []IntegrationName` accessor method on `RegistrationServiceConfig` in `pkg/configuration/configuration.go`. Note: unlike pointer-based fields (e.g. `UICanaryDeploymentWeight`, `WorkatoWebHookURL`) that use `commonconfig.Get*` helpers, this accessor returns the `[]IntegrationName` field directly — nil and empty both mean "nothing disabled," so no default-value wrapper is needed. |
| 150 | +2. Extend `UIConfigResponse` in `pkg/controller/uiconfig.go` to include the `DisabledIntegrations` field (typed as `[]toolchainv1alpha1.IntegrationName`). |
| 151 | +3. Update the `GetHandler` to populate the new field from configuration, normalizing nil to an empty slice. |
| 152 | +4. Add unit tests for the updated handler. |
| 153 | + |
| 154 | +### Phase 4: UI (out of scope for this design) |
| 155 | + |
| 156 | +The UI team will consume the `disabledIntegrations` field from the `/api/v1/uiconfig` response and hide any integration present in the array. |
| 157 | + |
| 158 | +## Decisions Summary |
| 159 | + |
| 160 | +| # | Question | Decision | |
| 161 | +|---|----------|----------| |
| 162 | +| Q1 | Allowlist vs. denylist | Denylist (`disabledIntegrations`) — only list broken integrations | |
| 163 | +| Q2 | Field type | Typed enum `[]IntegrationName` with kubebuilder validation | |
| 164 | +| Q3 | Field placement | Under `spec.host.registrationService` | |
| 165 | +| Q4 | Endpoint | Extend existing `GET /api/v1/uiconfig` | |
| 166 | +| Q5 | Authentication | Keep JWT-secured (no change) | |
| 167 | +| Q6 | CRD field name | `disabledIntegrations` | |
| 168 | +| Q7 | REST response property | Mirror CRD — `disabledIntegrations` | |
| 169 | + |
| 170 | +Full decision rationale: [enabled-integrations-questions.md](enabled-integrations-questions.md) |
0 commit comments