@owlmeans/kluster 0.1.0
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/LICENSE +21 -0
- package/README.md +640 -0
- package/build/.gitkeep +0 -0
- package/build/consts.d.ts +7 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +7 -0
- package/build/consts.js.map +1 -0
- package/build/helper.d.ts +5 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +26 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/service.d.ts +3 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +68 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +17 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +38 -0
- package/src/consts.ts +12 -0
- package/src/helper.ts +36 -0
- package/src/index.ts +5 -0
- package/src/service.ts +84 -0
- package/src/types.ts +22 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# @owlmeans/kluster
|
|
2
|
+
|
|
3
|
+
The **@owlmeans/kluster** package provides Kubernetes integration for OwlMeans Common Libraries, enabling applications to interact with Kubernetes clusters for service discovery, pod management, and configuration resolution in cloud-native environments.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package serves as a Kubernetes integration layer for OwlMeans applications, specifically designed for:
|
|
8
|
+
|
|
9
|
+
- **Service Discovery**: Automatically discover and connect to services running in Kubernetes clusters
|
|
10
|
+
- **Pod Management**: Query and interact with pods using label selectors
|
|
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.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @owlmeans/kluster
|
|
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')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `klusterize<C extends KlusterConfig, T extends BasicContext<C>>(context: T, alias?: string): T`
|
|
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')
|
|
90
|
+
|
|
91
|
+
**Returns:** Enhanced context with Kubernetes support
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { klusterize } from '@owlmeans/kluster'
|
|
95
|
+
import { makeServerContext } from '@owlmeans/server-context'
|
|
96
|
+
|
|
97
|
+
const context = makeServerContext(config)
|
|
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
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { createMiddleware } from '@owlmeans/kluster'
|
|
112
|
+
|
|
113
|
+
const middleware = createMiddleware('k8s-client')
|
|
114
|
+
context.registerMiddleware(middleware)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Service Methods
|
|
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
|
+
```
|
|
209
|
+
|
|
210
|
+
### Service Discovery
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { KlusterService } from '@owlmeans/kluster'
|
|
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)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Configuration File Resolution
|
|
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
|
|
491
|
+
|
|
492
|
+
services:
|
|
493
|
+
auth: kluster:service:app=auth-service
|
|
494
|
+
notification: kluster:service:app=notification-service
|
|
495
|
+
`
|
|
496
|
+
|
|
497
|
+
// Load and process configuration
|
|
498
|
+
import yaml from 'yaml'
|
|
499
|
+
|
|
500
|
+
const config = yaml.parse(yamlConfig)
|
|
501
|
+
const context = klusterize(makeServerContext(config))
|
|
502
|
+
|
|
503
|
+
// After initialization, all kluster directives are resolved
|
|
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'
|
|
519
|
+
|
|
520
|
+
async function robustServiceDiscovery(klusterService: KlusterService) {
|
|
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
|
+
```
|
|
616
|
+
|
|
617
|
+
## Best Practices
|
|
618
|
+
|
|
619
|
+
1. **Namespace Management**: Always specify appropriate namespaces for service discovery
|
|
620
|
+
2. **Label Consistency**: Use consistent labeling conventions across services
|
|
621
|
+
3. **Error Handling**: Implement robust error handling for network operations
|
|
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
|
|
626
|
+
|
|
627
|
+
## Dependencies
|
|
628
|
+
|
|
629
|
+
This package depends on:
|
|
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
|
|
635
|
+
|
|
636
|
+
## Related Packages
|
|
637
|
+
|
|
638
|
+
- [`@owlmeans/server-app`](../server-app) - Server application framework
|
|
639
|
+
- [`@owlmeans/server-context`](../server-context) - Server context management
|
|
640
|
+
- [`@owlmeans/config`](../config) - Configuration management system
|
package/build/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const DEFAULT_ALIAS = "kluster";
|
|
2
|
+
export declare const DIRECTIVE = "kluster";
|
|
3
|
+
export declare const SEP = ":";
|
|
4
|
+
export declare const DEFAULT_NAMESPACE = "default";
|
|
5
|
+
export declare const ACT_HOST = "hostname";
|
|
6
|
+
export declare const ACT_SERVICE = "service";
|
|
7
|
+
//# sourceMappingURL=consts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,eAAO,MAAM,SAAS,YAAY,CAAA;AAElC,eAAO,MAAM,GAAG,MAAM,CAAA;AAEtB,eAAO,MAAM,iBAAiB,YAAY,CAAA;AAE1C,eAAO,MAAM,QAAQ,aAAa,CAAA;AAElC,eAAO,MAAM,WAAW,YAAY,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAA;AAEtC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAElC,MAAM,CAAC,MAAM,GAAG,GAAG,GAAG,CAAA;AAEtB,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAA;AAE1C,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAA;AAElC,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BasicContext, Middleware } from '@owlmeans/context';
|
|
2
|
+
import type { KlusterConfig } from './types.js';
|
|
3
|
+
export declare const klusterize: <C extends KlusterConfig, T extends BasicContext<C>>(context: T, alias?: string) => T;
|
|
4
|
+
export declare const createMiddleware: (alias?: string) => Middleware;
|
|
5
|
+
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAIjE,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,YAAY,CAAA;AAE/D,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,aAAa,EAAE,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,UAAS,MAAM,KAAmB,CAQ1H,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,UAiBjD,CAAA"}
|
package/build/helper.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DEFAULT_ALIAS, DIRECTIVE, SEP } from './consts.js';
|
|
2
|
+
import { makeKlusterService } from './service.js';
|
|
3
|
+
import { MiddlewareStage, MiddlewareType } from '@owlmeans/context';
|
|
4
|
+
import { visitConfigLeafs } from '@owlmeans/config/utils';
|
|
5
|
+
export const klusterize = (context, alias = DEFAULT_ALIAS) => {
|
|
6
|
+
const service = makeKlusterService(alias);
|
|
7
|
+
context.registerService(service);
|
|
8
|
+
context.registerMiddleware(createMiddleware(alias));
|
|
9
|
+
return context;
|
|
10
|
+
};
|
|
11
|
+
export const createMiddleware = (alias = DEFAULT_ALIAS) => ({
|
|
12
|
+
type: MiddlewareType.Config,
|
|
13
|
+
stage: MiddlewareStage.Loading,
|
|
14
|
+
apply: async (context) => {
|
|
15
|
+
await visitConfigLeafs(context.cfg, async (value) => {
|
|
16
|
+
if (value.startsWith(DIRECTIVE + SEP)) {
|
|
17
|
+
const service = context.service(alias);
|
|
18
|
+
await service.ready?.();
|
|
19
|
+
const [, directive, query] = value.split(SEP, 3);
|
|
20
|
+
value = await service.dispatch(directive, query);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAIzD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAqD,OAAU,EAAE,QAAgB,aAAa,EAAK,EAAE;IAC7H,MAAM,OAAO,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAA;IAEzC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;IAEhC,OAAO,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;IAEnD,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAmC,CAAC,KAAK,GAAG,aAAa,EAAE,EAAE,CAAC,CAAC;IAC1F,IAAI,EAAE,cAAc,CAAC,MAAM;IAE3B,KAAK,EAAE,eAAe,CAAC,OAAO;IAE9B,KAAK,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QACrB,MAAM,gBAAgB,CAAC,OAAO,CAAC,GAAsB,EAAE,KAAK,EAAC,KAAK,EAAC,EAAE;YACnE,IAAI,KAAK,CAAC,UAAU,CAAC,SAAS,GAAG,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAiB,KAAK,CAAC,CAAA;gBACtD,MAAM,OAAO,CAAC,KAAK,EAAE,EAAE,CAAA;gBACvB,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;gBAChD,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;YAClD,CAAC;YAED,OAAO,KAAK,CAAA;QACd,CAAC,CAAC,CAAA;IACJ,CAAC;CACF,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
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,kBAAkB,WAAW,MAAM,KAAmB,cAyElE,CAAA"}
|
package/build/service.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ACT_HOST, ACT_SERVICE, DEFAULT_ALIAS, DEFAULT_NAMESPACE } from './consts.js';
|
|
2
|
+
import { assertContext, createLazyService } from '@owlmeans/context';
|
|
3
|
+
import { KubeConfig, CoreV1Api, HttpError, NetworkingV1Api } from '@kubernetes/client-node';
|
|
4
|
+
import { readConfigValue } from '@owlmeans/server-config';
|
|
5
|
+
export const makeKlusterService = (alias = DEFAULT_ALIAS) => {
|
|
6
|
+
const location = `kluster:${alias}`;
|
|
7
|
+
const updateSelector = (selector) => selector.startsWith('/') || selector.startsWith('file:')
|
|
8
|
+
? readConfigValue(selector, selector) : selector;
|
|
9
|
+
const service = createLazyService(alias, {
|
|
10
|
+
getHostnames: async (selector) => {
|
|
11
|
+
try {
|
|
12
|
+
const ctx = assertContext(service.ctx, location);
|
|
13
|
+
const namespace = ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE;
|
|
14
|
+
const { body } = await service.api.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector);
|
|
15
|
+
return body.items.map(item => item.status?.podIP).filter(name => name != null);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
if (e instanceof HttpError) {
|
|
19
|
+
console.error(e.name, e.cause, e.message, e.body);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
getServiceHostname: async (selector) => {
|
|
28
|
+
try {
|
|
29
|
+
const ctx = assertContext(service.ctx, location);
|
|
30
|
+
const namespace = ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE;
|
|
31
|
+
const { body } = await service.api.listNamespacedService(namespace, undefined, undefined, undefined, undefined, selector);
|
|
32
|
+
if (body.items[0] == null) {
|
|
33
|
+
return selector;
|
|
34
|
+
}
|
|
35
|
+
return body.items[0].metadata?.name ?? selector;
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof HttpError) {
|
|
39
|
+
console.error(e.name, e.cause, e.message, e.body);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
return selector;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
dispatch: async (action, query) => {
|
|
48
|
+
query = updateSelector(query);
|
|
49
|
+
switch (action) {
|
|
50
|
+
case ACT_HOST:
|
|
51
|
+
return service.getHostnames(query);
|
|
52
|
+
case ACT_SERVICE:
|
|
53
|
+
return service.getServiceHostname(query);
|
|
54
|
+
}
|
|
55
|
+
throw new SyntaxError(`Unknown kluster directive: ${action}`);
|
|
56
|
+
},
|
|
57
|
+
makeNetworkingApi: () => service.config.makeApiClient(NetworkingV1Api)
|
|
58
|
+
}, service => async () => {
|
|
59
|
+
if (service.config == null || service.api == null) {
|
|
60
|
+
service.config = new KubeConfig();
|
|
61
|
+
service.config.loadFromDefault();
|
|
62
|
+
service.api = service.config.makeApiClient(CoreV1Api);
|
|
63
|
+
}
|
|
64
|
+
service.initialized = true;
|
|
65
|
+
});
|
|
66
|
+
return service;
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LazyService } from '@owlmeans/context';
|
|
2
|
+
import type { KubeConfig, CoreV1Api, NetworkingV1Api } from '@kubernetes/client-node';
|
|
3
|
+
import type { ServerConfig } from '@owlmeans/server-context';
|
|
4
|
+
export interface KlusterService extends LazyService {
|
|
5
|
+
config?: KubeConfig;
|
|
6
|
+
api?: CoreV1Api;
|
|
7
|
+
getHostnames: (selector: string) => Promise<string[]>;
|
|
8
|
+
getServiceHostname: (selector: string) => Promise<string>;
|
|
9
|
+
dispatch: <T>(action: string, query: string) => Promise<T>;
|
|
10
|
+
makeNetworkingApi: () => NetworkingV1Api;
|
|
11
|
+
}
|
|
12
|
+
export interface KlusterConfig extends ServerConfig {
|
|
13
|
+
kluster?: {
|
|
14
|
+
namespace?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,GAAG,CAAC,EAAE,SAAS,CAAA;IAEf,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAErD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAEzD,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAE1D,iBAAiB,EAAE,MAAM,eAAe,CAAA;CACzC;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,OAAO,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/kluster",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 180 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@kubernetes/client-node": "^0.22.0",
|
|
24
|
+
"@owlmeans/config": "^0.1.0",
|
|
25
|
+
"@owlmeans/context": "^0.1.0",
|
|
26
|
+
"@owlmeans/server-config": "^0.1.0",
|
|
27
|
+
"@owlmeans/server-context": "^0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.7.8",
|
|
31
|
+
"nodemon": "^3.1.7",
|
|
32
|
+
"typescript": "^5.6.3"
|
|
33
|
+
},
|
|
34
|
+
"private": false,
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/consts.ts
ADDED
package/src/helper.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DEFAULT_ALIAS, DIRECTIVE, SEP } from './consts.js'
|
|
2
|
+
import { makeKlusterService } from './service.js'
|
|
3
|
+
import type { BasicContext, Middleware } from '@owlmeans/context'
|
|
4
|
+
import { MiddlewareStage, MiddlewareType } from '@owlmeans/context'
|
|
5
|
+
import { visitConfigLeafs } from '@owlmeans/config/utils'
|
|
6
|
+
import type { Tree } from '@owlmeans/config/utils'
|
|
7
|
+
import type { KlusterConfig, KlusterService } from './types.js'
|
|
8
|
+
|
|
9
|
+
export const klusterize = <C extends KlusterConfig, T extends BasicContext<C>>(context: T, alias: string = DEFAULT_ALIAS): T => {
|
|
10
|
+
const service = makeKlusterService(alias)
|
|
11
|
+
|
|
12
|
+
context.registerService(service)
|
|
13
|
+
|
|
14
|
+
context.registerMiddleware(createMiddleware(alias))
|
|
15
|
+
|
|
16
|
+
return context
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const createMiddleware: (alias?: string) => Middleware = (alias = DEFAULT_ALIAS) => ({
|
|
20
|
+
type: MiddlewareType.Config,
|
|
21
|
+
|
|
22
|
+
stage: MiddlewareStage.Loading,
|
|
23
|
+
|
|
24
|
+
apply: async context => {
|
|
25
|
+
await visitConfigLeafs(context.cfg as unknown as Tree, async value => {
|
|
26
|
+
if (value.startsWith(DIRECTIVE + SEP)) {
|
|
27
|
+
const service = context.service<KlusterService>(alias)
|
|
28
|
+
await service.ready?.()
|
|
29
|
+
const [, directive, query] = value.split(SEP, 3)
|
|
30
|
+
value = await service.dispatch(directive, query)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
})
|
package/src/index.ts
ADDED
package/src/service.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ACT_HOST, ACT_SERVICE, DEFAULT_ALIAS, DEFAULT_NAMESPACE } from './consts.js'
|
|
2
|
+
import type { KlusterConfig, KlusterService } from './types.js'
|
|
3
|
+
import { assertContext, createLazyService } from '@owlmeans/context'
|
|
4
|
+
import { KubeConfig, CoreV1Api, HttpError, NetworkingV1Api } from '@kubernetes/client-node'
|
|
5
|
+
import { ServerContext } from '@owlmeans/server-context'
|
|
6
|
+
import { readConfigValue } from '@owlmeans/server-config'
|
|
7
|
+
|
|
8
|
+
type Config = KlusterConfig
|
|
9
|
+
type Context = ServerContext<Config>
|
|
10
|
+
|
|
11
|
+
export const makeKlusterService = (alias: string = DEFAULT_ALIAS): KlusterService => {
|
|
12
|
+
const location = `kluster:${alias}`
|
|
13
|
+
|
|
14
|
+
const updateSelector = (selector: string): string =>
|
|
15
|
+
selector.startsWith('/') || selector.startsWith('file:')
|
|
16
|
+
? readConfigValue(selector, selector) : selector
|
|
17
|
+
|
|
18
|
+
const service: KlusterService = createLazyService<KlusterService>(alias, {
|
|
19
|
+
getHostnames: async selector => {
|
|
20
|
+
try {
|
|
21
|
+
const ctx = assertContext<Config, Context>(service.ctx as Context, location)
|
|
22
|
+
const namespace = ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE
|
|
23
|
+
|
|
24
|
+
const { body } = await service.api!.listNamespacedPod(
|
|
25
|
+
namespace, undefined, undefined, undefined, undefined, selector
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return body.items.map(item => item.status?.podIP).filter(name => name != null)
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e instanceof HttpError) {
|
|
31
|
+
console.error(e.name, e.cause, e.message, e.body)
|
|
32
|
+
} else {
|
|
33
|
+
throw e
|
|
34
|
+
}
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
getServiceHostname: async selector => {
|
|
40
|
+
try {
|
|
41
|
+
const ctx = assertContext<Config, Context>(service.ctx as Context, location)
|
|
42
|
+
const namespace = ctx.cfg.kluster?.namespace ?? DEFAULT_NAMESPACE
|
|
43
|
+
|
|
44
|
+
const { body } = await service.api!.listNamespacedService(namespace, undefined, undefined, undefined, undefined, selector)
|
|
45
|
+
|
|
46
|
+
if (body.items[0] == null) {
|
|
47
|
+
return selector
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return body.items[0].metadata?.name ?? selector
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e instanceof HttpError) {
|
|
53
|
+
console.error(e.name, e.cause, e.message, e.body)
|
|
54
|
+
} else {
|
|
55
|
+
throw e
|
|
56
|
+
}
|
|
57
|
+
return selector
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
dispatch: async <T>(action: string, query: string) => {
|
|
62
|
+
query = updateSelector(query)
|
|
63
|
+
switch (action) {
|
|
64
|
+
case ACT_HOST:
|
|
65
|
+
return service.getHostnames(query) as T
|
|
66
|
+
case ACT_SERVICE:
|
|
67
|
+
return service.getServiceHostname(query) as T
|
|
68
|
+
}
|
|
69
|
+
throw new SyntaxError(`Unknown kluster directive: ${action}`)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
makeNetworkingApi: () => service.config!.makeApiClient(NetworkingV1Api)
|
|
73
|
+
}, service => async () => {
|
|
74
|
+
if (service.config == null || service.api == null) {
|
|
75
|
+
service.config = new KubeConfig()
|
|
76
|
+
service.config.loadFromDefault()
|
|
77
|
+
service.api = service.config.makeApiClient(CoreV1Api)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
service.initialized = true
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return service
|
|
84
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LazyService } from '@owlmeans/context'
|
|
2
|
+
import type { KubeConfig, CoreV1Api, NetworkingV1Api } from '@kubernetes/client-node'
|
|
3
|
+
import type { ServerConfig } from '@owlmeans/server-context'
|
|
4
|
+
|
|
5
|
+
export interface KlusterService extends LazyService {
|
|
6
|
+
config?: KubeConfig
|
|
7
|
+
api?: CoreV1Api
|
|
8
|
+
|
|
9
|
+
getHostnames: (selector: string) => Promise<string[]>
|
|
10
|
+
|
|
11
|
+
getServiceHostname: (selector: string) => Promise<string>
|
|
12
|
+
|
|
13
|
+
dispatch: <T>(action: string, query: string) => Promise<T>
|
|
14
|
+
|
|
15
|
+
makeNetworkingApi: () => NetworkingV1Api
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface KlusterConfig extends ServerConfig {
|
|
19
|
+
kluster?: {
|
|
20
|
+
namespace?: string
|
|
21
|
+
}
|
|
22
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
7
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
8
|
+
"moduleResolution": "Bundler"
|
|
9
|
+
},
|
|
10
|
+
"exclude": [
|
|
11
|
+
"./dist/**/*",
|
|
12
|
+
"./build/**/*",
|
|
13
|
+
"./*.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/consts.ts","./src/helper.ts","./src/index.ts","./src/service.ts","./src/types.ts"],"version":"5.6.3"}
|