@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 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"}
@@ -0,0 +1,7 @@
1
+ export const DEFAULT_ALIAS = 'kluster';
2
+ export const DIRECTIVE = 'kluster';
3
+ export const SEP = ':';
4
+ export const DEFAULT_NAMESPACE = 'default';
5
+ export const ACT_HOST = 'hostname';
6
+ export const ACT_SERVICE = 'service';
7
+ //# sourceMappingURL=consts.js.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,5 @@
1
+ export * from './consts.js';
2
+ export * from './service.js';
3
+ export * from './types.js';
4
+ export * from './helper.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,5 @@
1
+ export * from './consts.js';
2
+ export * from './service.js';
3
+ export * from './types.js';
4
+ export * from './helper.js';
5
+ //# sourceMappingURL=index.js.map
@@ -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,3 @@
1
+ import type { KlusterService } from './types.js';
2
+ export declare const makeKlusterService: (alias?: string) => KlusterService;
3
+ //# sourceMappingURL=service.d.ts.map
@@ -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"}
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
@@ -0,0 +1,12 @@
1
+
2
+ export const DEFAULT_ALIAS = 'kluster'
3
+
4
+ export const DIRECTIVE = 'kluster'
5
+
6
+ export const SEP = ':'
7
+
8
+ export const DEFAULT_NAMESPACE = 'default'
9
+
10
+ export const ACT_HOST = 'hostname'
11
+
12
+ export const ACT_SERVICE = 'service'
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
@@ -0,0 +1,5 @@
1
+
2
+ export * from './consts.js'
3
+ export * from './service.js'
4
+ export * from './types.js'
5
+ export * from './helper.js'
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"}