@kuadrant/kuadrant-backstage-plugin-frontend 0.0.1-test.1-1593c3ec
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +491 -0
- package/dist/components/ApiAccessCard/ApiAccessCard.esm.js +42 -0
- package/dist/components/ApiAccessCard/ApiAccessCard.esm.js.map +1 -0
- package/dist/components/ApiAccessCard/index.esm.js +2 -0
- package/dist/components/ApiAccessCard/index.esm.js.map +1 -0
- package/dist/components/ApiKeyManagementTab/ApiKeyManagementTab.esm.js +441 -0
- package/dist/components/ApiKeyManagementTab/ApiKeyManagementTab.esm.js.map +1 -0
- package/dist/components/ApiKeyManagementTab/index.esm.js +2 -0
- package/dist/components/ApiKeyManagementTab/index.esm.js.map +1 -0
- package/dist/components/ApiProductInfoCard/ApiProductInfoCard.esm.js +47 -0
- package/dist/components/ApiProductInfoCard/ApiProductInfoCard.esm.js.map +1 -0
- package/dist/components/ApiProductInfoCard/index.esm.js +2 -0
- package/dist/components/ApiProductInfoCard/index.esm.js.map +1 -0
- package/dist/components/ApprovalQueueCard/ApprovalQueueCard.esm.js +349 -0
- package/dist/components/ApprovalQueueCard/ApprovalQueueCard.esm.js.map +1 -0
- package/dist/components/ApprovalQueueCard/index.esm.js +2 -0
- package/dist/components/ApprovalQueueCard/index.esm.js.map +1 -0
- package/dist/components/CreateAPIProductDialog/CreateAPIProductDialog.esm.js +289 -0
- package/dist/components/CreateAPIProductDialog/CreateAPIProductDialog.esm.js.map +1 -0
- package/dist/components/KuadrantPage/KuadrantPage.esm.js +198 -0
- package/dist/components/KuadrantPage/KuadrantPage.esm.js.map +1 -0
- package/dist/components/KuadrantPage/index.esm.js +2 -0
- package/dist/components/KuadrantPage/index.esm.js.map +1 -0
- package/dist/components/PermissionGate/PermissionGate.esm.js +33 -0
- package/dist/components/PermissionGate/PermissionGate.esm.js.map +1 -0
- package/dist/components/PlanPolicyDetailPage/PlanPolicyDetailPage.esm.js +89 -0
- package/dist/components/PlanPolicyDetailPage/PlanPolicyDetailPage.esm.js.map +1 -0
- package/dist/components/PlanPolicyDetailPage/index.esm.js +2 -0
- package/dist/components/PlanPolicyDetailPage/index.esm.js.map +1 -0
- package/dist/hooks/useUserRole.esm.js +49 -0
- package/dist/hooks/useUserRole.esm.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.esm.js +6 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +68 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/routes.esm.js +13 -0
- package/dist/routes.esm.js.map +1 -0
- package/package.json +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# Kuadrant Plugin for Backstage/RHDH
|
|
2
|
+
|
|
3
|
+
Backstage plugin for Kuadrant - enables developer portals for API access management using Kuadrant Gateway API primitives.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **API Access Management**: Request API keys for Kuadrant-protected APIs
|
|
8
|
+
- **Tiered Plans**: Support for multiple access tiers with different rate limits via PlanPolicy
|
|
9
|
+
- **User Identity**: Integrates with Backstage identity API for user-specific API keys
|
|
10
|
+
- **Policy Visibility**: View AuthPolicies, RateLimitPolicies, and PlanPolicies
|
|
11
|
+
- **API Key Management**: View, create, and delete API keys with show/hide toggles
|
|
12
|
+
- **Approval Workflow**: Platform engineers can approve/reject API access requests
|
|
13
|
+
- **APIProduct Integration**: Sync APIProduct custom resources from Kubernetes
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- Backstage/RHDH instance (v1.30+)
|
|
18
|
+
- Kubernetes cluster with Kuadrant Gateway API and CRDs installed
|
|
19
|
+
- Backend plugin (`@internal/plugin-kuadrant-backend`) configured and running
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### 1. Copy Plugins to Your Instance
|
|
24
|
+
|
|
25
|
+
Copy both plugins to your Backstage instance:
|
|
26
|
+
```bash
|
|
27
|
+
# Frontend plugin
|
|
28
|
+
cp -r plugins/kuadrant /path/to/your/backstage/plugins/
|
|
29
|
+
|
|
30
|
+
# Backend plugin
|
|
31
|
+
cp -r plugins/kuadrant-backend /path/to/your/backstage/plugins/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Install Dependencies
|
|
35
|
+
|
|
36
|
+
Add to your root `package.json` workspaces if needed, then install:
|
|
37
|
+
```bash
|
|
38
|
+
yarn install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Configure Backend
|
|
42
|
+
|
|
43
|
+
In `packages/backend/src/index.ts`, add the backend plugins:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Kuadrant backend plugins
|
|
47
|
+
backend.add(import('@internal/plugin-kuadrant-backend'));
|
|
48
|
+
backend.add(import('@internal/plugin-kuadrant-backend/alpha'));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The backend plugin provides:
|
|
52
|
+
- HTTP API endpoints at `/api/kuadrant/*`
|
|
53
|
+
- APIProduct entity provider for catalog integration
|
|
54
|
+
|
|
55
|
+
### 4. Configure Frontend
|
|
56
|
+
|
|
57
|
+
#### 4.1. Add Plugin Dependency
|
|
58
|
+
|
|
59
|
+
In `packages/app/package.json`:
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@internal/plugin-kuadrant": "0.1.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### 4.2. Add Route
|
|
69
|
+
|
|
70
|
+
In `packages/app/src/components/AppBase/AppBase.tsx`:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { KuadrantPage } from '@internal/plugin-kuadrant';
|
|
74
|
+
|
|
75
|
+
// Inside FlatRoutes:
|
|
76
|
+
<Route path="/kuadrant" element={<KuadrantPage />} />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### 4.3. Add Menu Item
|
|
80
|
+
|
|
81
|
+
In `packages/app/src/consts.ts`:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
export const DefaultMainMenuItems = {
|
|
85
|
+
menuItems: {
|
|
86
|
+
// ... existing items
|
|
87
|
+
'default.kuadrant': {
|
|
88
|
+
title: 'Kuadrant',
|
|
89
|
+
icon: 'extension',
|
|
90
|
+
to: 'kuadrant',
|
|
91
|
+
priority: 55,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### 4.4. Add Entity Page Components
|
|
98
|
+
|
|
99
|
+
In `packages/app/src/components/catalog/EntityPage/defaultTabs.tsx`:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Add imports
|
|
103
|
+
import {
|
|
104
|
+
EntityKuadrantApiKeysContent,
|
|
105
|
+
EntityKuadrantApiProductInfoContent,
|
|
106
|
+
} from '@internal/plugin-kuadrant';
|
|
107
|
+
|
|
108
|
+
// Add to defaultTabs object:
|
|
109
|
+
export const defaultTabs = {
|
|
110
|
+
// ... existing tabs
|
|
111
|
+
'/api-keys': {
|
|
112
|
+
title: 'API Keys',
|
|
113
|
+
mountPoint: 'entity.page.api-keys',
|
|
114
|
+
},
|
|
115
|
+
'/api-product-info': {
|
|
116
|
+
title: 'API Product Info',
|
|
117
|
+
mountPoint: 'entity.page.api-product-info',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Add to tabRules object:
|
|
122
|
+
export const tabRules = {
|
|
123
|
+
// ... existing rules
|
|
124
|
+
'/api-keys': {
|
|
125
|
+
if: isKind('api'),
|
|
126
|
+
},
|
|
127
|
+
'/api-product-info': {
|
|
128
|
+
if: (entity: Entity) => entity.kind === 'APIProduct',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Add to tabChildren object:
|
|
133
|
+
export const tabChildren = {
|
|
134
|
+
// ... existing children
|
|
135
|
+
'/api-keys': {
|
|
136
|
+
children: <EntityKuadrantApiKeysContent />,
|
|
137
|
+
},
|
|
138
|
+
'/api-product-info': {
|
|
139
|
+
children: <EntityKuadrantApiProductInfoContent />,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### 4.5. Add API Access Card to Overview (Optional)
|
|
145
|
+
|
|
146
|
+
In `packages/app/src/components/catalog/EntityPage/OverviewTabContent.tsx`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { EntityKuadrantApiAccessCard } from '@internal/plugin-kuadrant';
|
|
150
|
+
|
|
151
|
+
// In the EntitySwitch.Case for isKind('api'), add:
|
|
152
|
+
<Grid
|
|
153
|
+
item
|
|
154
|
+
sx={{
|
|
155
|
+
gridColumn: {
|
|
156
|
+
lg: '5 / -1',
|
|
157
|
+
md: '7 / -1',
|
|
158
|
+
xs: '1 / -1',
|
|
159
|
+
},
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<EntityKuadrantApiAccessCard />
|
|
163
|
+
</Grid>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 5. Configure Catalog
|
|
167
|
+
|
|
168
|
+
Update `app-config.yaml` to allow APIProduct entities:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
catalog:
|
|
172
|
+
rules:
|
|
173
|
+
- allow: [Component, System, Group, Resource, Location, Template, API, APIProduct]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 6. Configure Kubernetes Access
|
|
177
|
+
|
|
178
|
+
The backend plugin uses `@kubernetes/client-node` which supports multiple authentication methods:
|
|
179
|
+
|
|
180
|
+
#### Production (Explicit Cluster Configuration)
|
|
181
|
+
|
|
182
|
+
For production deployments, configure explicit cluster access in `app-config.yaml`:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
kubernetes:
|
|
186
|
+
clusterLocatorMethods:
|
|
187
|
+
- type: config
|
|
188
|
+
clusters:
|
|
189
|
+
- name: production
|
|
190
|
+
url: https://your-k8s-cluster-url
|
|
191
|
+
authProvider: serviceAccount
|
|
192
|
+
serviceAccountToken: ${K8S_CLUSTER_TOKEN}
|
|
193
|
+
skipTLSVerify: false # set to true only for development
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Environment variables:
|
|
197
|
+
- `K8S_CLUSTER_TOKEN` - Service account token for cluster access
|
|
198
|
+
|
|
199
|
+
**Required RBAC permissions:**
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
apiVersion: v1
|
|
203
|
+
kind: ServiceAccount
|
|
204
|
+
metadata:
|
|
205
|
+
name: rhdh-kuadrant
|
|
206
|
+
namespace: rhdh
|
|
207
|
+
---
|
|
208
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
209
|
+
kind: ClusterRole
|
|
210
|
+
metadata:
|
|
211
|
+
name: rhdh-kuadrant
|
|
212
|
+
rules:
|
|
213
|
+
# APIProduct and APIKeyRequest CRDs
|
|
214
|
+
- apiGroups: ["extensions.kuadrant.io"]
|
|
215
|
+
resources: ["apiproducts", "apikeyrequests"]
|
|
216
|
+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
|
217
|
+
# PlanPolicy CRDs
|
|
218
|
+
- apiGroups: ["kuadrant.io"]
|
|
219
|
+
resources: ["planpolicies"]
|
|
220
|
+
verbs: ["get", "list", "watch"]
|
|
221
|
+
# Secrets for API keys
|
|
222
|
+
- apiGroups: [""]
|
|
223
|
+
resources: ["secrets"]
|
|
224
|
+
verbs: ["get", "list", "create", "delete"]
|
|
225
|
+
---
|
|
226
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
227
|
+
kind: ClusterRoleBinding
|
|
228
|
+
metadata:
|
|
229
|
+
name: rhdh-kuadrant
|
|
230
|
+
roleRef:
|
|
231
|
+
apiGroup: rbac.authorization.k8s.io
|
|
232
|
+
kind: ClusterRole
|
|
233
|
+
name: rhdh-kuadrant
|
|
234
|
+
subjects:
|
|
235
|
+
- kind: ServiceAccount
|
|
236
|
+
name: rhdh-kuadrant
|
|
237
|
+
namespace: rhdh
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
To get the service account token:
|
|
241
|
+
```bash
|
|
242
|
+
kubectl create token rhdh-kuadrant -n rhdh --duration=8760h
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### In-Cluster (Automatic)
|
|
246
|
+
|
|
247
|
+
When RHDH runs inside Kubernetes without explicit configuration, it automatically uses the service account mounted at `/var/run/secrets/kubernetes.io/serviceaccount/`. Ensure the pod's service account has the RBAC permissions listed above.
|
|
248
|
+
|
|
249
|
+
#### Local Development
|
|
250
|
+
|
|
251
|
+
For local development, the plugin automatically uses your kubeconfig file (`~/.kube/config`). No configuration needed in `app-config.local.yaml`.
|
|
252
|
+
|
|
253
|
+
Verify access:
|
|
254
|
+
```bash
|
|
255
|
+
kubectl config current-context # verify cluster
|
|
256
|
+
kubectl get apiproducts -A # test access
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Configuring API Entities
|
|
260
|
+
|
|
261
|
+
To enable Kuadrant features for an API entity, add annotations:
|
|
262
|
+
|
|
263
|
+
```yaml
|
|
264
|
+
apiVersion: backstage.io/v1alpha1
|
|
265
|
+
kind: API
|
|
266
|
+
metadata:
|
|
267
|
+
name: toystore-api
|
|
268
|
+
annotations:
|
|
269
|
+
# required: name of the gateway api httproute resource
|
|
270
|
+
kuadrant.io/httproute: toystore
|
|
271
|
+
|
|
272
|
+
# required: kubernetes namespace where the httproute exists
|
|
273
|
+
kuadrant.io/namespace: toystore
|
|
274
|
+
|
|
275
|
+
# optional: gateway name for reference
|
|
276
|
+
kuadrant.io/gateway: external
|
|
277
|
+
spec:
|
|
278
|
+
type: openapi
|
|
279
|
+
lifecycle: production
|
|
280
|
+
owner: team-a
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Annotation Reference
|
|
284
|
+
|
|
285
|
+
| Annotation | Required | Description | Example |
|
|
286
|
+
|-----------|----------|-------------|---------|
|
|
287
|
+
| `kuadrant.io/httproute` | yes | Name of the Gateway API HTTPRoute resource | `toystore` |
|
|
288
|
+
| `kuadrant.io/namespace` | yes | Kubernetes namespace containing the HTTPRoute | `toystore` |
|
|
289
|
+
| `kuadrant.io/gateway` | no | Gateway name for reference/display | `external` |
|
|
290
|
+
|
|
291
|
+
## Usage
|
|
292
|
+
|
|
293
|
+
### For API Consumers
|
|
294
|
+
|
|
295
|
+
1. Navigate to an API entity in the catalog
|
|
296
|
+
2. Click the "API Keys" tab
|
|
297
|
+
3. Click "Request API Access"
|
|
298
|
+
4. Select a plan tier (bronze, silver, gold) and provide use case
|
|
299
|
+
5. Wait for approval from platform engineers
|
|
300
|
+
6. Once approved, your API key will appear in the API Keys tab
|
|
301
|
+
|
|
302
|
+
### For Platform Engineers
|
|
303
|
+
|
|
304
|
+
1. Navigate to the Kuadrant page from the sidebar menu
|
|
305
|
+
2. View "Pending API Key Requests" card
|
|
306
|
+
3. Review each request with details:
|
|
307
|
+
- Requester information
|
|
308
|
+
- API name and namespace
|
|
309
|
+
- Requested plan tier
|
|
310
|
+
- Use case justification
|
|
311
|
+
4. Approve or reject with optional comments
|
|
312
|
+
5. API keys are automatically created in Kubernetes upon approval
|
|
313
|
+
|
|
314
|
+
### For API Owners
|
|
315
|
+
|
|
316
|
+
1. Navigate to the Kuadrant page
|
|
317
|
+
2. View all API products synced from Kubernetes
|
|
318
|
+
3. Create new API products with:
|
|
319
|
+
- Display name and description
|
|
320
|
+
- Multiple plan tiers with rate limits
|
|
321
|
+
- Associated PlanPolicy references
|
|
322
|
+
- Contact information and documentation links
|
|
323
|
+
4. API products automatically sync to Backstage catalog as APIProduct entities
|
|
324
|
+
|
|
325
|
+
## Components
|
|
326
|
+
|
|
327
|
+
### Pages
|
|
328
|
+
|
|
329
|
+
- **`KuadrantPage`** - Main page showing API products list and approval queue
|
|
330
|
+
|
|
331
|
+
### Entity Content Components
|
|
332
|
+
|
|
333
|
+
- **`EntityKuadrantApiKeysContent`** - Full API keys management tab for API entities
|
|
334
|
+
- **`EntityKuadrantApiProductInfoContent`** - APIProduct details and plan information tab
|
|
335
|
+
- **`EntityKuadrantApiAccessCard`** - Quick API key request card for API entity overview
|
|
336
|
+
|
|
337
|
+
### Other Components
|
|
338
|
+
|
|
339
|
+
- **`ApprovalQueueCard`** - Displays pending API key requests for platform engineers
|
|
340
|
+
- **`CreateAPIProductDialog`** - Dialog for creating new API products
|
|
341
|
+
|
|
342
|
+
### Hooks
|
|
343
|
+
|
|
344
|
+
- **`useUserRole()`** - Determines user role based on Backstage groups:
|
|
345
|
+
- Platform Engineer: member of `platform-engineers` or `platform-admins`
|
|
346
|
+
- API Owner: member of `api-owners` or `app-developers`
|
|
347
|
+
- API Consumer: member of `api-consumers`
|
|
348
|
+
|
|
349
|
+
## Kubernetes Resources
|
|
350
|
+
|
|
351
|
+
The plugin creates and manages Kubernetes custom resources:
|
|
352
|
+
|
|
353
|
+
### APIKeyRequest
|
|
354
|
+
|
|
355
|
+
Created when users request API access:
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
apiVersion: extensions.kuadrant.io/v1alpha1
|
|
359
|
+
kind: APIKeyRequest
|
|
360
|
+
metadata:
|
|
361
|
+
name: guest-toystore-abc123
|
|
362
|
+
namespace: toystore
|
|
363
|
+
spec:
|
|
364
|
+
apiName: toystore
|
|
365
|
+
apiNamespace: toystore
|
|
366
|
+
planTier: silver
|
|
367
|
+
requestedAt: "2025-10-29T08:14:49.412Z"
|
|
368
|
+
requestedBy:
|
|
369
|
+
userId: guest
|
|
370
|
+
email: user@example.com
|
|
371
|
+
useCase: "Testing API integration"
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### API Key Secrets
|
|
375
|
+
|
|
376
|
+
Created upon approval:
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
apiVersion: v1
|
|
380
|
+
kind: Secret
|
|
381
|
+
metadata:
|
|
382
|
+
name: user-toystore-1234567890
|
|
383
|
+
namespace: toystore
|
|
384
|
+
labels:
|
|
385
|
+
app: toystore # Matches AuthPolicy selector
|
|
386
|
+
annotations:
|
|
387
|
+
secret.kuadrant.io/user-id: john
|
|
388
|
+
secret.kuadrant.io/plan-id: silver
|
|
389
|
+
type: Opaque
|
|
390
|
+
data:
|
|
391
|
+
api_key: <base64-encoded-key>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### APIProduct
|
|
395
|
+
|
|
396
|
+
Synced from Kubernetes to Backstage catalog:
|
|
397
|
+
|
|
398
|
+
```yaml
|
|
399
|
+
apiVersion: extensions.kuadrant.io/v1alpha1
|
|
400
|
+
kind: APIProduct
|
|
401
|
+
metadata:
|
|
402
|
+
name: toystore-api
|
|
403
|
+
namespace: toystore
|
|
404
|
+
spec:
|
|
405
|
+
displayName: Toystore API
|
|
406
|
+
description: Simple toy store API for demonstration
|
|
407
|
+
version: v1
|
|
408
|
+
plans:
|
|
409
|
+
- tier: gold
|
|
410
|
+
description: Premium access
|
|
411
|
+
limits:
|
|
412
|
+
daily: 100
|
|
413
|
+
- tier: silver
|
|
414
|
+
description: Standard access
|
|
415
|
+
limits:
|
|
416
|
+
daily: 50
|
|
417
|
+
planPolicyRef:
|
|
418
|
+
name: toystore-plans
|
|
419
|
+
namespace: toystore
|
|
420
|
+
contact:
|
|
421
|
+
team: platform-team
|
|
422
|
+
email: platform@example.com
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Development
|
|
426
|
+
|
|
427
|
+
### Running Locally
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
# Frontend plugin
|
|
431
|
+
cd plugins/kuadrant
|
|
432
|
+
yarn start
|
|
433
|
+
|
|
434
|
+
# Backend plugin
|
|
435
|
+
cd plugins/kuadrant-backend
|
|
436
|
+
yarn start
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Building
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
yarn build
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Testing
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
yarn test
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Linting
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
yarn lint:check
|
|
455
|
+
yarn lint:fix
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Permissions
|
|
459
|
+
|
|
460
|
+
The plugin integrates with Backstage permissions. Different views and actions are available based on user roles:
|
|
461
|
+
|
|
462
|
+
- **Platform Engineers**: Full access including approval queue
|
|
463
|
+
- **API Owners**: Can create and manage API products
|
|
464
|
+
- **API Consumers**: Can request API access and manage their keys
|
|
465
|
+
|
|
466
|
+
Roles are determined by Backstage catalog group membership.
|
|
467
|
+
|
|
468
|
+
## Troubleshooting
|
|
469
|
+
|
|
470
|
+
### "No pending requests" shown but requests exist in Kubernetes
|
|
471
|
+
|
|
472
|
+
Ensure the backend is using the correct backend URL. Check browser console for errors about non-JSON responses, which indicates the frontend is hitting the wrong endpoint.
|
|
473
|
+
|
|
474
|
+
### APIProduct entities not appearing in catalog
|
|
475
|
+
|
|
476
|
+
1. Check backend logs for APIProduct entity provider sync messages
|
|
477
|
+
2. Verify Kubernetes connectivity from the backend
|
|
478
|
+
3. Ensure APIProduct CRDs exist in your cluster
|
|
479
|
+
4. Check catalog rules allow APIProduct kind
|
|
480
|
+
|
|
481
|
+
### API key requests failing
|
|
482
|
+
|
|
483
|
+
1. Verify Kubernetes write permissions for the backend service account
|
|
484
|
+
2. Check backend logs for detailed error messages
|
|
485
|
+
3. Ensure the target namespace exists and is accessible
|
|
486
|
+
|
|
487
|
+
## Related Documentation
|
|
488
|
+
|
|
489
|
+
- [Backend Plugin README](../kuadrant-backend/README.md)
|
|
490
|
+
- [Kuadrant Documentation](https://docs.kuadrant.io/)
|
|
491
|
+
- [Backstage Plugin Development](https://backstage.io/docs/plugins/)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useAsync } from 'react-use';
|
|
3
|
+
import { Progress, ResponseErrorPanel, InfoCard } from '@backstage/core-components';
|
|
4
|
+
import { Box, Typography, Chip } from '@material-ui/core';
|
|
5
|
+
import { useApi, configApiRef, identityApiRef, fetchApiRef } from '@backstage/core-plugin-api';
|
|
6
|
+
import { useEntity } from '@backstage/plugin-catalog-react';
|
|
7
|
+
|
|
8
|
+
const ApiAccessCard = ({ namespace: propNamespace }) => {
|
|
9
|
+
const { entity } = useEntity();
|
|
10
|
+
const config = useApi(configApiRef);
|
|
11
|
+
const identityApi = useApi(identityApiRef);
|
|
12
|
+
const fetchApi = useApi(fetchApiRef);
|
|
13
|
+
const backendUrl = config.getString("backend.baseUrl");
|
|
14
|
+
const [userId, setUserId] = useState("guest");
|
|
15
|
+
const namespace = entity.metadata.annotations?.["kuadrant.io/namespace"] || propNamespace || "default";
|
|
16
|
+
useAsync(async () => {
|
|
17
|
+
const identity = await identityApi.getBackstageIdentity();
|
|
18
|
+
setUserId(identity.userEntityRef.split("/")[1] || "guest");
|
|
19
|
+
}, [identityApi]);
|
|
20
|
+
const { value: apiKeys, loading: keysLoading, error: keysError } = useAsync(async () => {
|
|
21
|
+
const response = await fetchApi.fetch(`${backendUrl}/api/kuadrant/apikeys?namespace=${namespace}&userId=${userId}`);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error("failed to fetch api keys");
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
return data.items || [];
|
|
27
|
+
}, [namespace, userId, backendUrl, fetchApi]);
|
|
28
|
+
if (keysLoading) {
|
|
29
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
30
|
+
}
|
|
31
|
+
if (keysError) {
|
|
32
|
+
return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error: keysError });
|
|
33
|
+
}
|
|
34
|
+
const keys = apiKeys || [];
|
|
35
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(InfoCard, { title: "Kuadrant API Keys" }, /* @__PURE__ */ React.createElement(Box, { p: 2 }, keys.length > 0 ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", gutterBottom: true }, "You have ", keys.length, " active API key", keys.length !== 1 ? "s" : "", " for this API"), keys.map((key) => {
|
|
36
|
+
const planTier = key.metadata.annotations?.["secret.kuadrant.io/plan-id"] || "Unknown";
|
|
37
|
+
return /* @__PURE__ */ React.createElement(Box, { key: key.metadata.name, mb: 1, p: 1, border: 1, borderColor: "grey.300", borderRadius: 4 }, /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "space-between", alignItems: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "body2" }, key.metadata.name), /* @__PURE__ */ React.createElement(Chip, { label: planTier, color: "primary", size: "small" })));
|
|
38
|
+
}), /* @__PURE__ */ React.createElement(Box, { mt: 2 }, /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "textSecondary" }, "Visit the API Keys tab to view keys, make new requests, or manage access"))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Typography, { variant: "body1", gutterBottom: true }, "You don't have any API keys for this API yet"), /* @__PURE__ */ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "Visit the API Keys tab to request access")))));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { ApiAccessCard };
|
|
42
|
+
//# sourceMappingURL=ApiAccessCard.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiAccessCard.esm.js","sources":["../../../src/components/ApiAccessCard/ApiAccessCard.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport {\n InfoCard,\n Progress,\n ResponseErrorPanel,\n} from '@backstage/core-components';\nimport {\n Typography,\n Box,\n Chip,\n} from '@material-ui/core';\nimport { useApi, configApiRef, identityApiRef, fetchApiRef } from '@backstage/core-plugin-api';\nimport { useEntity } from '@backstage/plugin-catalog-react';\n\ninterface ApiKey {\n metadata: {\n name: string;\n namespace: string;\n annotations?: {\n 'secret.kuadrant.io/plan-id'?: string;\n 'secret.kuadrant.io/user-id'?: string;\n };\n };\n data?: {\n api_key?: string;\n };\n}\n\nexport interface ApiAccessCardProps {\n // deprecated: use entity annotations instead\n namespace?: string;\n}\n\nexport const ApiAccessCard = ({ namespace: propNamespace }: ApiAccessCardProps) => {\n const { entity } = useEntity();\n const config = useApi(configApiRef);\n const identityApi = useApi(identityApiRef);\n const fetchApi = useApi(fetchApiRef);\n const backendUrl = config.getString('backend.baseUrl');\n const [userId, setUserId] = useState<string>('guest');\n\n // read from entity annotations, fallback to props for backwards compat\n const namespace = entity.metadata.annotations?.['kuadrant.io/namespace'] || propNamespace || 'default';\n\n // get current user identity\n useAsync(async () => {\n const identity = await identityApi.getBackstageIdentity();\n setUserId(identity.userEntityRef.split('/')[1] || 'guest');\n }, [identityApi]);\n\n const { value: apiKeys, loading: keysLoading, error: keysError } = useAsync(async () => {\n const response = await fetchApi.fetch(`${backendUrl}/api/kuadrant/apikeys?namespace=${namespace}&userId=${userId}`);\n if (!response.ok) {\n throw new Error('failed to fetch api keys');\n }\n const data = await response.json();\n return data.items || [];\n }, [namespace, userId, backendUrl, fetchApi]);\n\n if (keysLoading) {\n return <Progress />;\n }\n\n if (keysError) {\n return <ResponseErrorPanel error={keysError} />;\n }\n\n const keys = (apiKeys as ApiKey[]) || [];\n\n return (\n <>\n <InfoCard title=\"Kuadrant API Keys\">\n <Box p={2}>\n {keys.length > 0 ? (\n <>\n <Typography variant=\"body1\" gutterBottom>\n You have {keys.length} active API key{keys.length !== 1 ? 's' : ''} for this API\n </Typography>\n {keys.map((key: ApiKey) => {\n const planTier = key.metadata.annotations?.['secret.kuadrant.io/plan-id'] || 'Unknown';\n\n return (\n <Box key={key.metadata.name} mb={1} p={1} border={1} borderColor=\"grey.300\" borderRadius={4}>\n <Box display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\">\n <Typography variant=\"body2\">\n {key.metadata.name}\n </Typography>\n <Chip label={planTier} color=\"primary\" size=\"small\" />\n </Box>\n </Box>\n );\n })}\n <Box mt={2}>\n <Typography variant=\"caption\" color=\"textSecondary\">\n Visit the API Keys tab to view keys, make new requests, or manage access\n </Typography>\n </Box>\n </>\n ) : (\n <>\n <Typography variant=\"body1\" gutterBottom>\n You don't have any API keys for this API yet\n </Typography>\n <Typography variant=\"body2\" color=\"textSecondary\">\n Visit the API Keys tab to request access\n </Typography>\n </>\n )}\n </Box>\n </InfoCard>\n </>\n );\n};\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|