@owlmeans/kluster 0.1.2 → 0.1.3
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 +33 -606
- package/build/service.d.ts +1 -0
- package/build/service.d.ts.map +1 -1
- package/build/service.js +26 -8
- package/build/service.js.map +1 -1
- package/package.json +8 -7
- package/src/service.ts +22 -13
- package/tsconfig.json +5 -5
package/README.md
CHANGED
|
@@ -1,640 +1,67 @@
|
|
|
1
1
|
# @owlmeans/kluster
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Kubernetes integration service — service discovery and pod/service IP resolution for cloud-native OwlMeans apps.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- **Configuration Resolution**: Resolve configuration values from Kubernetes resources at runtime
|
|
12
|
-
- **Cloud-native Deployment**: Enable applications to work seamlessly in Kubernetes environments
|
|
13
|
-
- **Dynamic Service Endpoints**: Automatically resolve service hostnames and endpoints
|
|
14
|
-
- **Network API Integration**: Access Kubernetes networking APIs for advanced operations
|
|
15
|
-
|
|
16
|
-
## Key Concepts
|
|
17
|
-
|
|
18
|
-
### Kubernetes Service Discovery
|
|
19
|
-
The package provides automatic service discovery within Kubernetes clusters, allowing applications to find and connect to other services without hardcoded endpoints.
|
|
20
|
-
|
|
21
|
-
### Configuration Directives
|
|
22
|
-
Special configuration syntax (`kluster:directive:query`) that gets resolved at runtime using Kubernetes API calls, enabling dynamic configuration based on cluster state.
|
|
23
|
-
|
|
24
|
-
### Lazy Initialization
|
|
25
|
-
Kubernetes client is initialized lazily when first accessed, reducing startup time and resource usage when Kubernetes features are not immediately needed.
|
|
26
|
-
|
|
27
|
-
### Label Selector Queries
|
|
28
|
-
Uses Kubernetes label selectors to query for pods and services, providing flexible and powerful resource discovery capabilities.
|
|
7
|
+
- `makeKlusterService(alias?)` — creates a Kubernetes API client service
|
|
8
|
+
- `klusterize(context, alias?)` — registers the service and its config-resolution middleware
|
|
9
|
+
- `KlusterService` — resolves pod hostnames and service cluster IPs, dispatches `kluster:` directives
|
|
10
|
+
- Used to resolve `kluster:<selector>` config values to actual pod IPs at runtime
|
|
29
11
|
|
|
30
12
|
## Installation
|
|
31
13
|
|
|
32
14
|
```bash
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## API Reference
|
|
37
|
-
|
|
38
|
-
### Types
|
|
39
|
-
|
|
40
|
-
#### `KlusterService`
|
|
41
|
-
Main service interface for Kubernetes operations.
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
interface KlusterService extends LazyService {
|
|
45
|
-
config?: KubeConfig
|
|
46
|
-
api?: CoreV1Api
|
|
47
|
-
|
|
48
|
-
getHostnames(selector: string): Promise<string[]>
|
|
49
|
-
getServiceHostname(selector: string): Promise<string>
|
|
50
|
-
dispatch<T>(action: string, query: string): Promise<T>
|
|
51
|
-
makeNetworkingApi(): NetworkingV1Api
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
#### `KlusterConfig`
|
|
56
|
-
Configuration interface extending ServerConfig.
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
interface KlusterConfig extends ServerConfig {
|
|
60
|
-
kluster?: {
|
|
61
|
-
namespace?: string
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Core Functions
|
|
67
|
-
|
|
68
|
-
#### `makeKlusterService(alias?: string): KlusterService`
|
|
69
|
-
|
|
70
|
-
Creates a Kubernetes service instance with lazy initialization.
|
|
71
|
-
|
|
72
|
-
**Parameters:**
|
|
73
|
-
- `alias`: Service alias (defaults to 'kluster')
|
|
74
|
-
|
|
75
|
-
**Returns:** KlusterService instance
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
import { makeKlusterService } from '@owlmeans/kluster'
|
|
79
|
-
|
|
80
|
-
const klusterService = makeKlusterService('k8s-client')
|
|
15
|
+
bun add @owlmeans/kluster
|
|
81
16
|
```
|
|
82
17
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Enhances a context with Kubernetes capabilities and registers necessary middleware.
|
|
86
|
-
|
|
87
|
-
**Parameters:**
|
|
88
|
-
- `context`: Application context to enhance
|
|
89
|
-
- `alias`: Service alias (defaults to 'kluster')
|
|
18
|
+
## Usage
|
|
90
19
|
|
|
91
|
-
|
|
20
|
+
Register in server context setup:
|
|
92
21
|
|
|
93
22
|
```typescript
|
|
94
|
-
import { klusterize } from '@owlmeans/kluster'
|
|
95
|
-
import {
|
|
23
|
+
import { klusterize, DEFAULT_ALIAS as KLUSTER_SERVICE } from '@owlmeans/kluster'
|
|
24
|
+
import type { KlusterService } from '@owlmeans/kluster'
|
|
96
25
|
|
|
97
|
-
|
|
98
|
-
const k8sContext = klusterize(context)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
#### `createMiddleware(alias?: string): Middleware`
|
|
102
|
-
|
|
103
|
-
Creates middleware for processing Kubernetes configuration directives.
|
|
104
|
-
|
|
105
|
-
**Parameters:**
|
|
106
|
-
- `alias`: Service alias (defaults to 'kluster')
|
|
107
|
-
|
|
108
|
-
**Returns:** Middleware instance
|
|
26
|
+
klusterize<C, T>(context)
|
|
109
27
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const middleware = createMiddleware('k8s-client')
|
|
114
|
-
context.registerMiddleware(middleware)
|
|
28
|
+
// Access in context type
|
|
29
|
+
context.kluster = () => context.service<KlusterService>(KLUSTER_SERVICE)
|
|
115
30
|
```
|
|
116
31
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
#### `getHostnames(selector: string): Promise<string[]>`
|
|
120
|
-
|
|
121
|
-
Retrieves pod IP addresses using a label selector.
|
|
122
|
-
|
|
123
|
-
**Parameters:**
|
|
124
|
-
- `selector`: Kubernetes label selector
|
|
125
|
-
|
|
126
|
-
**Returns:** Promise resolving to array of pod IP addresses
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
const klusterService = context.service<KlusterService>('kluster')
|
|
130
|
-
const podIPs = await klusterService.getHostnames('app=redis,tier=cache')
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### `getServiceHostname(selector: string): Promise<string>`
|
|
134
|
-
|
|
135
|
-
Retrieves service hostname using a label selector.
|
|
136
|
-
|
|
137
|
-
**Parameters:**
|
|
138
|
-
- `selector`: Kubernetes label selector
|
|
139
|
-
|
|
140
|
-
**Returns:** Promise resolving to service hostname
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
const serviceName = await klusterService.getServiceHostname('app=database')
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
#### `dispatch<T>(action: string, query: string): Promise<T>`
|
|
147
|
-
|
|
148
|
-
Dispatches actions for configuration resolution.
|
|
149
|
-
|
|
150
|
-
**Parameters:**
|
|
151
|
-
- `action`: Action type ('hostname' or 'service')
|
|
152
|
-
- `query`: Query string (label selector or config path)
|
|
153
|
-
|
|
154
|
-
**Returns:** Promise resolving to action result
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
const result = await klusterService.dispatch('service', 'app=api-gateway')
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
#### `makeNetworkingApi(): NetworkingV1Api`
|
|
161
|
-
|
|
162
|
-
Creates a Kubernetes Networking API client.
|
|
163
|
-
|
|
164
|
-
**Returns:** NetworkingV1Api instance
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
const networkingApi = klusterService.makeNetworkingApi()
|
|
168
|
-
const ingresses = await networkingApi.listIngressForAllNamespaces()
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Constants
|
|
172
|
-
|
|
173
|
-
#### Service Configuration
|
|
174
|
-
```typescript
|
|
175
|
-
const DEFAULT_ALIAS = 'kluster'
|
|
176
|
-
const DEFAULT_NAMESPACE = 'default'
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
#### Configuration Directives
|
|
180
|
-
```typescript
|
|
181
|
-
const DIRECTIVE = 'kluster'
|
|
182
|
-
const SEP = ':'
|
|
183
|
-
const ACT_HOST = 'hostname'
|
|
184
|
-
const ACT_SERVICE = 'service'
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## Usage Examples
|
|
188
|
-
|
|
189
|
-
### Basic Kubernetes Integration
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
import { klusterize } from '@owlmeans/kluster'
|
|
193
|
-
import { makeServerContext, makeServerConfig } from '@owlmeans/server-context'
|
|
194
|
-
|
|
195
|
-
// Create server configuration with Kubernetes settings
|
|
196
|
-
const config = makeServerConfig('k8s-app', {
|
|
197
|
-
kluster: {
|
|
198
|
-
namespace: 'production'
|
|
199
|
-
},
|
|
200
|
-
port: 8080
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// Create and enhance context with Kubernetes capabilities
|
|
204
|
-
const context = makeServerContext(config)
|
|
205
|
-
const k8sContext = klusterize(context)
|
|
206
|
-
|
|
207
|
-
await k8sContext.configure().init()
|
|
208
|
-
```
|
|
32
|
+
Config values can reference Kubernetes selectors:
|
|
209
33
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Get Kubernetes service
|
|
216
|
-
const klusterService = context.service<KlusterService>('kluster')
|
|
217
|
-
|
|
218
|
-
// Discover database service
|
|
219
|
-
const dbServiceName = await klusterService.getServiceHostname('app=postgres,tier=database')
|
|
220
|
-
console.log('Database service:', dbServiceName)
|
|
221
|
-
|
|
222
|
-
// Get all Redis cache pod IPs
|
|
223
|
-
const redisIPs = await klusterService.getHostnames('app=redis,tier=cache')
|
|
224
|
-
console.log('Redis pods:', redisIPs)
|
|
225
|
-
|
|
226
|
-
// Connect to discovered service
|
|
227
|
-
const dbConnection = createConnection({
|
|
228
|
-
host: dbServiceName,
|
|
229
|
-
port: 5432,
|
|
230
|
-
database: 'myapp'
|
|
231
|
-
})
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Configuration Directives
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
// Configuration with Kubernetes directives
|
|
238
|
-
const config = makeServerConfig('microservice', {
|
|
239
|
-
kluster: {
|
|
240
|
-
namespace: 'production'
|
|
241
|
-
},
|
|
242
|
-
services: {
|
|
243
|
-
// These will be resolved at runtime using Kubernetes API
|
|
244
|
-
database: 'kluster:service:app=postgres',
|
|
245
|
-
cache: 'kluster:service:app=redis',
|
|
246
|
-
messageQueue: 'kluster:service:app=rabbitmq'
|
|
247
|
-
},
|
|
248
|
-
externalServices: {
|
|
249
|
-
// Resolve to actual pod IPs for load balancing
|
|
250
|
-
apiNodes: 'kluster:hostname:app=api,tier=backend'
|
|
251
|
-
}
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// After context initialization, directives are resolved
|
|
255
|
-
await context.configure().init()
|
|
256
|
-
|
|
257
|
-
// Now services contain actual Kubernetes service names
|
|
258
|
-
console.log(context.cfg.services.database) // e.g., "postgres-service"
|
|
259
|
-
console.log(context.cfg.externalServices.apiNodes) // e.g., ["10.1.1.5", "10.1.1.6"]
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Microservice Communication
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
import { klusterize, KlusterService } from '@owlmeans/kluster'
|
|
266
|
-
|
|
267
|
-
// Setup microservice with Kubernetes discovery
|
|
268
|
-
const config = makeServerConfig('user-service', {
|
|
269
|
-
kluster: { namespace: 'microservices' },
|
|
270
|
-
downstream: {
|
|
271
|
-
authService: 'kluster:service:app=auth-service',
|
|
272
|
-
emailService: 'kluster:service:app=email-service',
|
|
273
|
-
notificationService: 'kluster:service:app=notification-service'
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
const context = klusterize(makeServerContext(config))
|
|
278
|
-
await context.configure().init()
|
|
279
|
-
|
|
280
|
-
// Use discovered services
|
|
281
|
-
async function createUser(userData: UserData) {
|
|
282
|
-
// Call authentication service
|
|
283
|
-
const authResponse = await fetch(`http://${context.cfg.downstream.authService}/validate`, {
|
|
284
|
-
method: 'POST',
|
|
285
|
-
headers: { 'Content-Type': 'application/json' },
|
|
286
|
-
body: JSON.stringify(userData)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
if (authResponse.ok) {
|
|
290
|
-
const user = await saveUser(userData)
|
|
291
|
-
|
|
292
|
-
// Send welcome email
|
|
293
|
-
await fetch(`http://${context.cfg.downstream.emailService}/send`, {
|
|
294
|
-
method: 'POST',
|
|
295
|
-
body: JSON.stringify({
|
|
296
|
-
to: user.email,
|
|
297
|
-
template: 'welcome',
|
|
298
|
-
data: { userName: user.name }
|
|
299
|
-
})
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
return user
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
throw new Error('Authentication failed')
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Load Balancing with Pod Discovery
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
import { KlusterService } from '@owlmeans/kluster'
|
|
313
|
-
|
|
314
|
-
class LoadBalancedClient {
|
|
315
|
-
private klusterService: KlusterService
|
|
316
|
-
private endpoints: string[] = []
|
|
317
|
-
private currentIndex = 0
|
|
318
|
-
|
|
319
|
-
constructor(klusterService: KlusterService, private selector: string) {
|
|
320
|
-
this.klusterService = klusterService
|
|
321
|
-
this.refreshEndpoints()
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private async refreshEndpoints() {
|
|
325
|
-
try {
|
|
326
|
-
this.endpoints = await this.klusterService.getHostnames(this.selector)
|
|
327
|
-
console.log(`Discovered ${this.endpoints.length} endpoints`)
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.error('Failed to refresh endpoints:', error)
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private getNextEndpoint(): string {
|
|
334
|
-
if (this.endpoints.length === 0) {
|
|
335
|
-
throw new Error('No available endpoints')
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const endpoint = this.endpoints[this.currentIndex]
|
|
339
|
-
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length
|
|
340
|
-
return endpoint
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async makeRequest(path: string, options: RequestInit = {}) {
|
|
344
|
-
const endpoint = this.getNextEndpoint()
|
|
345
|
-
const url = `http://${endpoint}:8080${path}`
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
const response = await fetch(url, options)
|
|
349
|
-
return response
|
|
350
|
-
} catch (error) {
|
|
351
|
-
console.error(`Request to ${endpoint} failed:`, error)
|
|
352
|
-
// Refresh endpoints and retry
|
|
353
|
-
await this.refreshEndpoints()
|
|
354
|
-
throw error
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Usage
|
|
360
|
-
const klusterService = context.service<KlusterService>('kluster')
|
|
361
|
-
const apiClient = new LoadBalancedClient(klusterService, 'app=api,tier=backend')
|
|
362
|
-
|
|
363
|
-
const response = await apiClient.makeRequest('/health')
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Health Monitoring
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
import { KlusterService } from '@owlmeans/kluster'
|
|
370
|
-
|
|
371
|
-
class HealthMonitor {
|
|
372
|
-
constructor(
|
|
373
|
-
private klusterService: KlusterService,
|
|
374
|
-
private services: Record<string, string>
|
|
375
|
-
) {}
|
|
376
|
-
|
|
377
|
-
async checkServiceHealth(): Promise<Record<string, boolean>> {
|
|
378
|
-
const healthStatus: Record<string, boolean> = {}
|
|
379
|
-
|
|
380
|
-
for (const [serviceName, selector] of Object.entries(this.services)) {
|
|
381
|
-
try {
|
|
382
|
-
const serviceHost = await this.klusterService.getServiceHostname(selector)
|
|
383
|
-
const response = await fetch(`http://${serviceHost}/health`, {
|
|
384
|
-
timeout: 5000
|
|
385
|
-
})
|
|
386
|
-
healthStatus[serviceName] = response.ok
|
|
387
|
-
} catch (error) {
|
|
388
|
-
console.error(`Health check failed for ${serviceName}:`, error)
|
|
389
|
-
healthStatus[serviceName] = false
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return healthStatus
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async checkPodHealth(): Promise<Record<string, number>> {
|
|
397
|
-
const podCounts: Record<string, number> = {}
|
|
398
|
-
|
|
399
|
-
for (const [serviceName, selector] of Object.entries(this.services)) {
|
|
400
|
-
try {
|
|
401
|
-
const podIPs = await this.klusterService.getHostnames(selector)
|
|
402
|
-
podCounts[serviceName] = podIPs.length
|
|
403
|
-
} catch (error) {
|
|
404
|
-
console.error(`Pod discovery failed for ${serviceName}:`, error)
|
|
405
|
-
podCounts[serviceName] = 0
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return podCounts
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Setup health monitoring
|
|
414
|
-
const healthMonitor = new HealthMonitor(klusterService, {
|
|
415
|
-
'database': 'app=postgres',
|
|
416
|
-
'cache': 'app=redis',
|
|
417
|
-
'api': 'app=api,tier=backend'
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
// Monitor health every 30 seconds
|
|
421
|
-
setInterval(async () => {
|
|
422
|
-
const serviceHealth = await healthMonitor.checkServiceHealth()
|
|
423
|
-
const podCounts = await healthMonitor.checkPodHealth()
|
|
424
|
-
|
|
425
|
-
console.log('Service Health:', serviceHealth)
|
|
426
|
-
console.log('Pod Counts:', podCounts)
|
|
427
|
-
}, 30000)
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### Ingress Management
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
import { KlusterService } from '@owlmeans/kluster'
|
|
434
|
-
|
|
435
|
-
async function manageIngress(klusterService: KlusterService) {
|
|
436
|
-
const networkingApi = klusterService.makeNetworkingApi()
|
|
437
|
-
|
|
438
|
-
// List all ingresses
|
|
439
|
-
const ingresses = await networkingApi.listIngressForAllNamespaces()
|
|
440
|
-
console.log('Available ingresses:', ingresses.body.items.map(i => i.metadata?.name))
|
|
441
|
-
|
|
442
|
-
// Create new ingress
|
|
443
|
-
const ingressManifest = {
|
|
444
|
-
apiVersion: 'networking.k8s.io/v1',
|
|
445
|
-
kind: 'Ingress',
|
|
446
|
-
metadata: {
|
|
447
|
-
name: 'api-ingress',
|
|
448
|
-
namespace: 'production'
|
|
449
|
-
},
|
|
450
|
-
spec: {
|
|
451
|
-
rules: [{
|
|
452
|
-
host: 'api.example.com',
|
|
453
|
-
http: {
|
|
454
|
-
paths: [{
|
|
455
|
-
path: '/',
|
|
456
|
-
pathType: 'Prefix',
|
|
457
|
-
backend: {
|
|
458
|
-
service: {
|
|
459
|
-
name: 'api-service',
|
|
460
|
-
port: { number: 80 }
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}]
|
|
464
|
-
}
|
|
465
|
-
}]
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
await networkingApi.createNamespacedIngress('production', ingressManifest)
|
|
471
|
-
console.log('Ingress created successfully')
|
|
472
|
-
} catch (error) {
|
|
473
|
-
console.error('Failed to create ingress:', error)
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"dbs": {
|
|
37
|
+
"mongo": { "url": "kluster:app=mongo-svc" }
|
|
474
38
|
}
|
|
475
39
|
}
|
|
476
40
|
```
|
|
477
41
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
// config.yaml
|
|
482
|
-
const yamlConfig = `
|
|
483
|
-
database:
|
|
484
|
-
host: kluster:service:app=postgres
|
|
485
|
-
port: 5432
|
|
486
|
-
replicas: kluster:hostname:app=postgres
|
|
487
|
-
|
|
488
|
-
cache:
|
|
489
|
-
primary: kluster:service:app=redis-primary
|
|
490
|
-
replicas: kluster:hostname:app=redis-replica
|
|
42
|
+
The middleware resolves `kluster:` prefixed values to real cluster IPs at startup.
|
|
491
43
|
|
|
492
|
-
|
|
493
|
-
auth: kluster:service:app=auth-service
|
|
494
|
-
notification: kluster:service:app=notification-service
|
|
495
|
-
`
|
|
44
|
+
## API
|
|
496
45
|
|
|
497
|
-
|
|
498
|
-
import yaml from 'yaml'
|
|
46
|
+
### `makeKlusterService(alias?): KlusterService`
|
|
499
47
|
|
|
500
|
-
|
|
501
|
-
const context = klusterize(makeServerContext(config))
|
|
48
|
+
Creates the Kubernetes service. Reads in-cluster config or `~/.kube/config`.
|
|
502
49
|
|
|
503
|
-
|
|
504
|
-
await context.configure().init()
|
|
505
|
-
|
|
506
|
-
console.log('Resolved configuration:', context.cfg)
|
|
507
|
-
// Output:
|
|
508
|
-
// {
|
|
509
|
-
// database: { host: 'postgres-service', port: 5432, replicas: ['10.1.1.10', '10.1.1.11'] },
|
|
510
|
-
// cache: { primary: 'redis-primary-service', replicas: ['10.1.2.20', '10.1.2.21'] },
|
|
511
|
-
// services: { auth: 'auth-service', notification: 'notification-service' }
|
|
512
|
-
// }
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
## Error Handling
|
|
516
|
-
|
|
517
|
-
```typescript
|
|
518
|
-
import { KlusterService } from '@owlmeans/kluster'
|
|
50
|
+
### `klusterize<C, T>(context, alias?): T`
|
|
519
51
|
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
// Attempt service discovery
|
|
523
|
-
const serviceName = await klusterService.getServiceHostname('app=api')
|
|
524
|
-
return serviceName
|
|
525
|
-
} catch (error) {
|
|
526
|
-
console.error('Service discovery failed:', error)
|
|
527
|
-
|
|
528
|
-
// Fallback to default service name
|
|
529
|
-
return 'api-service'
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Handle network errors gracefully
|
|
534
|
-
async function resilientPodDiscovery(klusterService: KlusterService, selector: string) {
|
|
535
|
-
const maxRetries = 3
|
|
536
|
-
let retries = 0
|
|
537
|
-
|
|
538
|
-
while (retries < maxRetries) {
|
|
539
|
-
try {
|
|
540
|
-
const pods = await klusterService.getHostnames(selector)
|
|
541
|
-
if (pods.length > 0) {
|
|
542
|
-
return pods
|
|
543
|
-
}
|
|
544
|
-
throw new Error('No pods found')
|
|
545
|
-
} catch (error) {
|
|
546
|
-
retries++
|
|
547
|
-
console.warn(`Pod discovery attempt ${retries} failed:`, error.message)
|
|
548
|
-
|
|
549
|
-
if (retries < maxRetries) {
|
|
550
|
-
// Exponential backoff
|
|
551
|
-
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retries) * 1000))
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
throw new Error(`Failed to discover pods after ${maxRetries} attempts`)
|
|
557
|
-
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
## Integration Patterns
|
|
561
|
-
|
|
562
|
-
### Deployment Configuration
|
|
563
|
-
|
|
564
|
-
```yaml
|
|
565
|
-
# kubernetes-config.yaml
|
|
566
|
-
apiVersion: v1
|
|
567
|
-
kind: ConfigMap
|
|
568
|
-
metadata:
|
|
569
|
-
name: app-config
|
|
570
|
-
namespace: production
|
|
571
|
-
data:
|
|
572
|
-
config.json: |
|
|
573
|
-
{
|
|
574
|
-
"database": "kluster:service:app=postgres",
|
|
575
|
-
"cache": "kluster:service:app=redis",
|
|
576
|
-
"messageQueue": "kluster:service:app=rabbitmq"
|
|
577
|
-
}
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
```typescript
|
|
581
|
-
// Application code
|
|
582
|
-
import { klusterize } from '@owlmeans/kluster'
|
|
583
|
-
|
|
584
|
-
const config = makeServerConfig('app', {
|
|
585
|
-
kluster: { namespace: 'production' },
|
|
586
|
-
// Configuration loaded from ConfigMap
|
|
587
|
-
database: 'kluster:service:app=postgres',
|
|
588
|
-
cache: 'kluster:service:app=redis'
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
const context = klusterize(makeServerContext(config))
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
### Helm Chart Integration
|
|
595
|
-
|
|
596
|
-
```yaml
|
|
597
|
-
# values.yaml
|
|
598
|
-
app:
|
|
599
|
-
namespace: {{ .Values.namespace }}
|
|
600
|
-
services:
|
|
601
|
-
database: "kluster:service:app=postgres"
|
|
602
|
-
cache: "kluster:service:app=redis"
|
|
603
|
-
|
|
604
|
-
# deployment.yaml
|
|
605
|
-
apiVersion: apps/v1
|
|
606
|
-
kind: Deployment
|
|
607
|
-
spec:
|
|
608
|
-
template:
|
|
609
|
-
spec:
|
|
610
|
-
containers:
|
|
611
|
-
- name: app
|
|
612
|
-
env:
|
|
613
|
-
- name: KLUSTER_NAMESPACE
|
|
614
|
-
value: {{ .Values.namespace }}
|
|
615
|
-
```
|
|
52
|
+
Registers the kluster service and a middleware that resolves `kluster:` directives in config.
|
|
616
53
|
|
|
617
|
-
|
|
54
|
+
### `KlusterService`
|
|
618
55
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
4. **Caching**: Consider caching service discovery results to reduce API calls
|
|
623
|
-
5. **Security**: Ensure proper RBAC permissions for Kubernetes API access
|
|
624
|
-
6. **Monitoring**: Monitor service discovery operations and cluster health
|
|
625
|
-
7. **Fallbacks**: Provide fallback mechanisms when service discovery fails
|
|
56
|
+
- `getHostnames(selector, namespace?): Promise<string[]>` — list pod IPs matching a label selector
|
|
57
|
+
- `getServiceHostname(selector, namespace?): Promise<string | null>` — get a service's cluster IP
|
|
58
|
+
- `dispatch<T>(action, query): Promise<T>` — dispatch a kluster directive query
|
|
626
59
|
|
|
627
|
-
|
|
60
|
+
### `KlusterConfig`
|
|
628
61
|
|
|
629
|
-
|
|
630
|
-
- `@owlmeans/config` - Configuration management
|
|
631
|
-
- `@owlmeans/context` - Context management system
|
|
632
|
-
- `@owlmeans/server-config` - Server configuration utilities
|
|
633
|
-
- `@owlmeans/server-context` - Server context management
|
|
634
|
-
- `@kubernetes/client-node` - Official Kubernetes client library
|
|
62
|
+
Extends `ServerConfig` with optional `namespace: string`.
|
|
635
63
|
|
|
636
64
|
## Related Packages
|
|
637
65
|
|
|
638
|
-
- [`@owlmeans/server-app`](../server-app)
|
|
639
|
-
- [`@owlmeans/
|
|
640
|
-
- [`@owlmeans/config`](../config) - Configuration management system
|
|
66
|
+
- [`@owlmeans/server-app`](../server-app) — `makeContext` where `klusterize` is typically called
|
|
67
|
+
- [`@owlmeans/mongo`](../mongo) — MongoDB URL can be a `kluster:` directive
|
package/build/service.d.ts
CHANGED
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,YAAY,CAAA;AAS/D,eAAO,MAAM,eAAe,GAAI,GAAG,OAAO,KAAG,OAa5C,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,QAAO,MAAsB,KAAG,cAqElE,CAAA"}
|
package/build/service.js
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { ACT_HOST, ACT_SERVICE, DEFAULT_ALIAS, DEFAULT_NAMESPACE } from './consts.js';
|
|
2
2
|
import { assertContext, createLazyService } from '@owlmeans/context';
|
|
3
|
-
import { KubeConfig, CoreV1Api,
|
|
3
|
+
import { KubeConfig, CoreV1Api, ApiException, NetworkingV1Api, AppsV1Api } from '@kubernetes/client-node';
|
|
4
4
|
import { readConfigValue } from '@owlmeans/server-config';
|
|
5
|
+
export const isNotFoundError = (e) => {
|
|
6
|
+
if (e == null || typeof e !== 'object')
|
|
7
|
+
return false;
|
|
8
|
+
const err = e;
|
|
9
|
+
// 1.4.0 ApiException uses .code
|
|
10
|
+
if (err['code'] === 404)
|
|
11
|
+
return true;
|
|
12
|
+
// 0.22.x HttpError used .statusCode
|
|
13
|
+
if (err['statusCode'] === 404)
|
|
14
|
+
return true;
|
|
15
|
+
// Some shapes nest status in .response or .body
|
|
16
|
+
const body = err['body'];
|
|
17
|
+
if (body?.['code'] === 404)
|
|
18
|
+
return true;
|
|
19
|
+
const response = err['response'];
|
|
20
|
+
if (response?.['statusCode'] === 404)
|
|
21
|
+
return true;
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
5
24
|
export const makeKlusterService = (alias = DEFAULT_ALIAS) => {
|
|
6
25
|
const location = `kluster:${alias}`;
|
|
7
26
|
const updateSelector = (selector) => selector.startsWith('/') || selector.startsWith('file:')
|
|
@@ -11,11 +30,11 @@ export const makeKlusterService = (alias = DEFAULT_ALIAS) => {
|
|
|
11
30
|
try {
|
|
12
31
|
const ctx = assertContext(service.ctx, location);
|
|
13
32
|
namespace ??= ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE;
|
|
14
|
-
const
|
|
15
|
-
return
|
|
33
|
+
const result = await service.api.listNamespacedPod({ namespace, labelSelector: selector });
|
|
34
|
+
return result.items.map(item => item.status?.podIP).filter(name => name != null);
|
|
16
35
|
}
|
|
17
36
|
catch (e) {
|
|
18
|
-
if (e instanceof
|
|
37
|
+
if (e instanceof ApiException) {
|
|
19
38
|
console.error(e.name, e.cause, e.message, e.body);
|
|
20
39
|
}
|
|
21
40
|
else {
|
|
@@ -28,12 +47,11 @@ export const makeKlusterService = (alias = DEFAULT_ALIAS) => {
|
|
|
28
47
|
try {
|
|
29
48
|
const ctx = assertContext(service.ctx, location);
|
|
30
49
|
namespace ??= ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE;
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
return body.items?.[0]?.spec?.clusterIP ?? null;
|
|
50
|
+
const result = await service.api.listNamespacedService({ namespace, labelSelector: selector });
|
|
51
|
+
return result.items?.[0]?.spec?.clusterIP ?? null;
|
|
34
52
|
}
|
|
35
53
|
catch (e) {
|
|
36
|
-
if (e instanceof
|
|
54
|
+
if (e instanceof ApiException) {
|
|
37
55
|
console.error(e.name, e.cause, e.message, e.body);
|
|
38
56
|
}
|
|
39
57
|
else {
|
package/build/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAErF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAErF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAEzG,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAKzD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAU,EAAW,EAAE;IACrD,IAAI,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACpD,MAAM,GAAG,GAAG,CAA4B,CAAA;IACxC,gCAAgC;IAChC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IACpC,oCAAoC;IACpC,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAC1C,gDAAgD;IAChD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAwC,CAAA;IAC/D,IAAI,IAAI,EAAE,CAAC,MAAM,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IACvC,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAwC,CAAA;IACvE,IAAI,QAAQ,EAAE,CAAC,YAAY,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IACjD,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,QAAgB,aAAa,EAAkB,EAAE;IAClF,MAAM,QAAQ,GAAG,WAAW,KAAK,EAAE,CAAA;IAEnC,MAAM,cAAc,GAAG,CAAC,QAAgB,EAAU,EAAE,CAClD,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;QACtD,CAAC,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAEpD,MAAM,OAAO,GAAmB,iBAAiB,CAAiB,KAAK,EAAE;QACvE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE;YAC1C,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,aAAa,CAAkB,OAAO,CAAC,GAAc,EAAE,QAAQ,CAAC,CAAA;gBAC5E,SAAS,KAAK,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,IAAI,iBAAiB,CAAA;gBAE7D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAI,CAAC,iBAAiB,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAA;gBAE3F,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,IAAI,CAAC,CAAA;YAClF,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,YAAY,EAAE,CAAC;oBAC9B,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;gBACnD,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,CAAA;gBACT,CAAC;gBACD,OAAO,EAAE,CAAA;YACX,CAAC;QACH,CAAC;QAED,kBAAkB,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE;YAChD,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,aAAa,CAAkB,OAAO,CAAC,GAAc,EAAE,QAAQ,CAAC,CAAA;gBAC5E,SAAS,KAAK,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,IAAI,iBAAiB,CAAA;gBAE7D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAI,CAAC,qBAAqB,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAA;gBAE/F,OAAO,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,IAAI,IAAI,CAAA;YACnD,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,YAAY,EAAE,CAAC;oBAC9B,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;gBACnD,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,CAAA;gBACT,CAAC;gBACD,OAAO,QAAQ,CAAA;YACjB,CAAC;QACH,CAAC;QAED,QAAQ,EAAE,KAAK,EAAK,MAAc,EAAE,KAAa,EAAE,EAAE;YACnD,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;YAC7B,QAAQ,MAAM,EAAE,CAAC;gBACf,KAAK,QAAQ;oBACX,OAAO,OAAO,CAAC,YAAY,CAAC,KAAK,CAAM,CAAA;gBACzC,KAAK,WAAW;oBACd,OAAO,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAM,CAAA;YACjD,CAAC;YACD,MAAM,IAAI,WAAW,CAAC,8BAA8B,MAAM,EAAE,CAAC,CAAA;QAC/D,CAAC;QAED,iBAAiB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAO,CAAC,aAAa,CAAC,eAAe,CAAC;QAEvE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAO,CAAC,aAAa,CAAC,SAAS,CAAC;KAC5D,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE;QACvB,IAAI,OAAO,CAAC,MAAM,IAAI,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;YAClD,OAAO,CAAC,MAAM,GAAG,IAAI,UAAU,EAAE,CAAA;YACjC,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,CAAA;YAChC,OAAO,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAA;QACvD,CAAC;QAED,OAAO,CAAC,WAAW,GAAG,IAAI,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/kluster",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,16 +21,17 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@kubernetes/client-node": "^
|
|
25
|
-
"@owlmeans/config": "^0.1.
|
|
26
|
-
"@owlmeans/context": "^0.1.
|
|
27
|
-
"@owlmeans/server-config": "^0.1.
|
|
28
|
-
"@owlmeans/server-context": "^0.1.
|
|
24
|
+
"@kubernetes/client-node": "^1.4.0",
|
|
25
|
+
"@owlmeans/config": "^0.1.3",
|
|
26
|
+
"@owlmeans/context": "^0.1.3",
|
|
27
|
+
"@owlmeans/server-config": "^0.1.3",
|
|
28
|
+
"@owlmeans/server-context": "^0.1.3"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
31
32
|
"@types/node": "^24.10.1",
|
|
32
33
|
"nodemon": "^3.1.11",
|
|
33
|
-
"typescript": "^
|
|
34
|
+
"typescript": "^6.0.2"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
package/src/service.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { ACT_HOST, ACT_SERVICE, DEFAULT_ALIAS, DEFAULT_NAMESPACE } from './consts.js'
|
|
2
2
|
import type { KlusterConfig, KlusterService } from './types.js'
|
|
3
3
|
import { assertContext, createLazyService } from '@owlmeans/context'
|
|
4
|
-
import { KubeConfig, CoreV1Api,
|
|
4
|
+
import { KubeConfig, CoreV1Api, ApiException, NetworkingV1Api, AppsV1Api } from '@kubernetes/client-node'
|
|
5
5
|
import { ServerContext } from '@owlmeans/server-context'
|
|
6
6
|
import { readConfigValue } from '@owlmeans/server-config'
|
|
7
7
|
|
|
8
|
-
// import util from 'node:util'
|
|
9
|
-
|
|
10
8
|
type Config = KlusterConfig
|
|
11
9
|
type Context = ServerContext<Config>
|
|
12
10
|
|
|
11
|
+
export const isNotFoundError = (e: unknown): boolean => {
|
|
12
|
+
if (e == null || typeof e !== 'object') return false
|
|
13
|
+
const err = e as Record<string, unknown>
|
|
14
|
+
// 1.4.0 ApiException uses .code
|
|
15
|
+
if (err['code'] === 404) return true
|
|
16
|
+
// 0.22.x HttpError used .statusCode
|
|
17
|
+
if (err['statusCode'] === 404) return true
|
|
18
|
+
// Some shapes nest status in .response or .body
|
|
19
|
+
const body = err['body'] as Record<string, unknown> | undefined
|
|
20
|
+
if (body?.['code'] === 404) return true
|
|
21
|
+
const response = err['response'] as Record<string, unknown> | undefined
|
|
22
|
+
if (response?.['statusCode'] === 404) return true
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
export const makeKlusterService = (alias: string = DEFAULT_ALIAS): KlusterService => {
|
|
14
27
|
const location = `kluster:${alias}`
|
|
15
28
|
|
|
@@ -23,13 +36,11 @@ export const makeKlusterService = (alias: string = DEFAULT_ALIAS): KlusterServic
|
|
|
23
36
|
const ctx = assertContext<Config, Context>(service.ctx as Context, location)
|
|
24
37
|
namespace ??= ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE
|
|
25
38
|
|
|
26
|
-
const
|
|
27
|
-
namespace, undefined, undefined, undefined, undefined, selector
|
|
28
|
-
)
|
|
39
|
+
const result = await service.api!.listNamespacedPod({ namespace, labelSelector: selector })
|
|
29
40
|
|
|
30
|
-
return
|
|
41
|
+
return result.items.map(item => item.status?.podIP).filter(name => name != null)
|
|
31
42
|
} catch (e) {
|
|
32
|
-
if (e instanceof
|
|
43
|
+
if (e instanceof ApiException) {
|
|
33
44
|
console.error(e.name, e.cause, e.message, e.body)
|
|
34
45
|
} else {
|
|
35
46
|
throw e
|
|
@@ -43,13 +54,11 @@ export const makeKlusterService = (alias: string = DEFAULT_ALIAS): KlusterServic
|
|
|
43
54
|
const ctx = assertContext<Config, Context>(service.ctx as Context, location)
|
|
44
55
|
namespace ??= ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE
|
|
45
56
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
// console.log(util.inspect(body, { depth: null, colors: true }))
|
|
57
|
+
const result = await service.api!.listNamespacedService({ namespace, labelSelector: selector })
|
|
49
58
|
|
|
50
|
-
return
|
|
59
|
+
return result.items?.[0]?.spec?.clusterIP ?? null
|
|
51
60
|
} catch (e) {
|
|
52
|
-
if (e instanceof
|
|
61
|
+
if (e instanceof ApiException) {
|
|
53
62
|
console.error(e.name, e.cause, e.message, e.body)
|
|
54
63
|
} else {
|
|
55
64
|
throw e
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": [
|
|
3
|
-
"
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.node.json"
|
|
4
5
|
],
|
|
5
6
|
"compilerOptions": {
|
|
6
|
-
"rootDir": "./src/",
|
|
7
|
-
"outDir": "./build/"
|
|
8
|
-
"moduleResolution": "Bundler"
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
9
|
},
|
|
10
10
|
"exclude": [
|
|
11
11
|
"./dist/**/*",
|
|
12
12
|
"./build/**/*",
|
|
13
13
|
"./*.ts"
|
|
14
14
|
]
|
|
15
|
-
}
|
|
15
|
+
}
|