@kapeta/local-cluster-service 0.34.2 → 0.35.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/.github/workflows/check-license.yml +0 -1
- package/CHANGELOG.md +7 -0
- package/dist/cjs/src/config/routes.js +9 -0
- package/dist/cjs/src/containerManager.d.ts +2 -8
- package/dist/cjs/src/containerManager.js +4 -4
- package/dist/cjs/src/instanceManager.d.ts +2 -1
- package/dist/cjs/src/instanceManager.js +43 -1
- package/dist/cjs/src/operatorManager.d.ts +2 -2
- package/dist/cjs/src/operatorManager.js +2 -7
- package/dist/cjs/src/serviceManager.d.ts +1 -0
- package/dist/cjs/src/serviceManager.js +9 -9
- package/dist/cjs/src/types.d.ts +39 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +16 -12
- package/dist/cjs/src/utils/utils.d.ts +5 -1
- package/dist/cjs/src/utils/utils.js +11 -1
- package/dist/esm/src/config/routes.js +9 -0
- package/dist/esm/src/containerManager.d.ts +2 -8
- package/dist/esm/src/containerManager.js +4 -4
- package/dist/esm/src/instanceManager.d.ts +2 -1
- package/dist/esm/src/instanceManager.js +43 -1
- package/dist/esm/src/operatorManager.d.ts +2 -2
- package/dist/esm/src/operatorManager.js +2 -7
- package/dist/esm/src/serviceManager.d.ts +1 -0
- package/dist/esm/src/serviceManager.js +9 -9
- package/dist/esm/src/types.d.ts +39 -0
- package/dist/esm/src/utils/BlockInstanceRunner.js +16 -12
- package/dist/esm/src/utils/utils.d.ts +5 -1
- package/dist/esm/src/utils/utils.js +11 -1
- package/package.json +1 -1
- package/src/config/routes.ts +15 -0
- package/src/containerManager.ts +5 -12
- package/src/instanceManager.ts +72 -4
- package/src/operatorManager.ts +5 -13
- package/src/serviceManager.ts +11 -8
- package/src/types.ts +35 -0
- package/src/utils/BlockInstanceRunner.ts +21 -14
- package/src/utils/utils.ts +13 -2
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# [0.35.0](https://github.com/kapetacom/local-cluster-service/compare/v0.34.2...v0.35.0) (2024-01-31)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* Adds support for getting instance operator info ([#122](https://github.com/kapetacom/local-cluster-service/issues/122)) ([8fa18ac](https://github.com/kapetacom/local-cluster-service/commit/8fa18ac58226ce9776f79a64d3a99cf56456e834))
|
7
|
+
|
1
8
|
## [0.34.2](https://github.com/kapetacom/local-cluster-service/compare/v0.34.1...v0.34.2) (2024-01-22)
|
2
9
|
|
3
10
|
|
@@ -148,4 +148,13 @@ router.get('/consumes/resource/:resourceType/:portType/:name', async (req, res)
|
|
148
148
|
router.get('/consumes/:resourceName/:type', (req, res) => {
|
149
149
|
res.send(serviceManager_1.serviceManager.getConsumerAddress(req.kapeta.systemId, req.kapeta.instanceId, req.params.resourceName, req.params.type, req.kapeta.environment));
|
150
150
|
});
|
151
|
+
/**
|
152
|
+
* Used by services to information about a block operator
|
153
|
+
*
|
154
|
+
* If the remote service is not already running it will be started
|
155
|
+
*/
|
156
|
+
router.get('/operator/:instanceId', async (req, res) => {
|
157
|
+
const operatorInfo = await instanceManager_1.instanceManager.getInstanceOperator(req.kapeta.systemId, req.params.instanceId, req.kapeta.environment);
|
158
|
+
res.send(operatorInfo);
|
159
|
+
});
|
151
160
|
exports.default = router;
|
@@ -5,7 +5,7 @@
|
|
5
5
|
/// <reference types="node" />
|
6
6
|
import FSExtra from 'fs-extra';
|
7
7
|
import Docker from 'dockerode';
|
8
|
-
import { InstanceInfo, LogEntry } from './types';
|
8
|
+
import { Health, InstanceInfo, LogEntry } from './types';
|
9
9
|
type StringMap = {
|
10
10
|
[key: string]: string;
|
11
11
|
};
|
@@ -26,12 +26,6 @@ export interface DockerMounts {
|
|
26
26
|
}
|
27
27
|
export type DockerContainerStatus = 'created' | 'running' | 'paused' | 'restarting' | 'removing' | 'exited' | 'dead';
|
28
28
|
export type DockerContainerHealth = 'starting' | 'healthy' | 'unhealthy' | 'none';
|
29
|
-
interface Health {
|
30
|
-
cmd: string;
|
31
|
-
interval?: number;
|
32
|
-
timeout?: number;
|
33
|
-
retries?: number;
|
34
|
-
}
|
35
29
|
export declare const CONTAINER_LABEL_PORT_PREFIX = "kapeta_port-";
|
36
30
|
export declare const COMPOSE_LABEL_PROJECT = "com.docker.compose.project";
|
37
31
|
export declare const COMPOSE_LABEL_SERVICE = "com.docker.compose.service";
|
@@ -50,7 +44,7 @@ declare class ContainerManager {
|
|
50
44
|
isAlive(): boolean;
|
51
45
|
getMountPoint(systemId: string, ref: string, mountName: string): string;
|
52
46
|
createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap>;
|
53
|
-
createVolumes(systemId: string,
|
47
|
+
createVolumes(systemId: string, serviceId: string, mountOpts: StringMap | null | undefined): Promise<DockerMounts[]>;
|
54
48
|
ping(): Promise<void>;
|
55
49
|
docker(): Docker;
|
56
50
|
getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
|
@@ -168,12 +168,12 @@ class ContainerManager {
|
|
168
168
|
}
|
169
169
|
return mounts;
|
170
170
|
}
|
171
|
-
async createVolumes(systemId,
|
171
|
+
async createVolumes(systemId, serviceId, mountOpts) {
|
172
172
|
const Mounts = [];
|
173
173
|
if (mountOpts) {
|
174
174
|
const mountOptList = Object.entries(mountOpts);
|
175
175
|
for (const [mountName, containerPath] of mountOptList) {
|
176
|
-
const volumeName = `${systemId}_${
|
176
|
+
const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
|
177
177
|
Mounts.push({
|
178
178
|
Target: containerPath,
|
179
179
|
Source: volumeName,
|
@@ -182,7 +182,7 @@ class ContainerManager {
|
|
182
182
|
Consistency: 'consistent',
|
183
183
|
Labels: {
|
184
184
|
[exports.COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
|
185
|
-
[exports.COMPOSE_LABEL_SERVICE]:
|
185
|
+
[exports.COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
|
186
186
|
},
|
187
187
|
});
|
188
188
|
}
|
@@ -791,7 +791,7 @@ class ContainerInfo {
|
|
791
791
|
if (!name.startsWith(exports.CONTAINER_LABEL_PORT_PREFIX)) {
|
792
792
|
return;
|
793
793
|
}
|
794
|
-
const hostPort = name.
|
794
|
+
const hostPort = name.substring(exports.CONTAINER_LABEL_PORT_PREFIX.length);
|
795
795
|
portTypes[hostPort] = portType;
|
796
796
|
});
|
797
797
|
lodash_1.default.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
|
@@ -2,7 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { InstanceInfo, LogEntry } from './types';
|
5
|
+
import { EnvironmentType, InstanceInfo, LogEntry, OperatorInstanceInfo } from './types';
|
6
6
|
import { Task } from './taskManager';
|
7
7
|
export declare class InstanceManager {
|
8
8
|
private _interval;
|
@@ -27,6 +27,7 @@ export declare class InstanceManager {
|
|
27
27
|
stop(systemId: string, instanceId: string): Promise<void>;
|
28
28
|
private stopInner;
|
29
29
|
stopAllForPlan(systemId: string): Task<void>;
|
30
|
+
getInstanceOperator(systemId: string, instanceId: string, environment?: EnvironmentType): Promise<OperatorInstanceInfo>;
|
30
31
|
start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
|
31
32
|
/**
|
32
33
|
* Stops an instance but does not remove it from the list of active instances
|
@@ -323,6 +323,48 @@ class InstanceManager {
|
|
323
323
|
name: `Stopping plan ${systemId}`,
|
324
324
|
});
|
325
325
|
}
|
326
|
+
async getInstanceOperator(systemId, instanceId, environment) {
|
327
|
+
const blockInstance = await assetManager_1.assetManager.getBlockInstance(systemId, instanceId);
|
328
|
+
if (!blockInstance) {
|
329
|
+
throw new Error(`Instance not found: ${systemId}/${instanceId}`);
|
330
|
+
}
|
331
|
+
const blockRef = (0, nodejs_utils_1.normalizeKapetaUri)(blockInstance.block.ref);
|
332
|
+
const block = await assetManager_1.assetManager.getAsset(blockRef, true);
|
333
|
+
if (!block) {
|
334
|
+
throw new Error(`Block not found: ${blockRef}`);
|
335
|
+
}
|
336
|
+
const operatorDefinition = await definitionsManager_1.definitionsManager.getDefinition(block.kind);
|
337
|
+
if (!operatorDefinition?.definition.spec.local) {
|
338
|
+
throw new Error(`Operator block has no local definition: ${blockRef}`);
|
339
|
+
}
|
340
|
+
const localConfig = operatorDefinition.definition.spec.local;
|
341
|
+
let instance = await this.start(systemId, instanceId);
|
342
|
+
if (instance instanceof taskManager_1.Task) {
|
343
|
+
instance = await instance.wait();
|
344
|
+
}
|
345
|
+
const container = await containerManager_1.containerManager.get(instance.pid);
|
346
|
+
if (!container) {
|
347
|
+
throw new Error(`Container not found: ${instance.pid}`);
|
348
|
+
}
|
349
|
+
const portInfo = await container.getPorts();
|
350
|
+
if (!portInfo) {
|
351
|
+
throw new Error(`No ports found for instance: ${instanceId}`);
|
352
|
+
}
|
353
|
+
const hostname = serviceManager_1.serviceManager.getLocalHost(environment);
|
354
|
+
const ports = {};
|
355
|
+
Object.entries(portInfo).forEach(([key, value]) => {
|
356
|
+
ports[key] = {
|
357
|
+
protocol: value.protocol,
|
358
|
+
port: parseInt(value.hostPort),
|
359
|
+
};
|
360
|
+
});
|
361
|
+
return {
|
362
|
+
hostname,
|
363
|
+
ports,
|
364
|
+
credentials: localConfig.credentials,
|
365
|
+
options: localConfig.options,
|
366
|
+
};
|
367
|
+
}
|
326
368
|
async start(systemId, instanceId) {
|
327
369
|
return this.exclusive(systemId, instanceId, async () => {
|
328
370
|
systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
|
@@ -333,7 +375,7 @@ class InstanceManager {
|
|
333
375
|
throw new Error('Block not found: ' + blockRef);
|
334
376
|
}
|
335
377
|
const existingInstance = this.getInstance(systemId, instanceId);
|
336
|
-
if (existingInstance) {
|
378
|
+
if (existingInstance && existingInstance.pid) {
|
337
379
|
if (existingInstance.status === types_1.InstanceStatus.READY) {
|
338
380
|
// Instance is already running
|
339
381
|
return existingInstance;
|
@@ -4,12 +4,12 @@
|
|
4
4
|
*/
|
5
5
|
import { DefinitionInfo } from '@kapeta/local-cluster-config';
|
6
6
|
import { ContainerInfo } from './containerManager';
|
7
|
-
import { EnvironmentType, OperatorInfo } from './types';
|
7
|
+
import { EnvironmentType, LocalImageOptions, OperatorInfo } from './types';
|
8
8
|
export declare const KIND_OPERATOR = "core/resource-type-operator";
|
9
9
|
declare class Operator {
|
10
10
|
private readonly _data;
|
11
11
|
constructor(data: DefinitionInfo);
|
12
|
-
getLocalData():
|
12
|
+
getLocalData(): LocalImageOptions;
|
13
13
|
getDefinitionInfo(): DefinitionInfo;
|
14
14
|
getCredentials(): any;
|
15
15
|
}
|
@@ -137,13 +137,8 @@ class OperatorManager {
|
|
137
137
|
const portType = portTypes[i];
|
138
138
|
let containerPortInfo = operatorData.ports[portType];
|
139
139
|
const hostPort = await serviceManager_1.serviceManager.ensureServicePort(systemId, resourceType, portType);
|
140
|
-
|
141
|
-
|
142
|
-
}
|
143
|
-
if (!containerPortInfo.type) {
|
144
|
-
containerPortInfo.type = 'tcp';
|
145
|
-
}
|
146
|
-
const portId = containerPortInfo.port + '/' + containerPortInfo.type;
|
140
|
+
const portInfo = (0, utils_1.toPortInfo)(containerPortInfo);
|
141
|
+
const portId = portInfo.port + '/' + portInfo.type;
|
147
142
|
ports[portId] = {
|
148
143
|
type: portType,
|
149
144
|
hostPort,
|
@@ -9,6 +9,7 @@ export declare const HTTP_PORTS: string[];
|
|
9
9
|
declare class ServiceManager {
|
10
10
|
private _systems;
|
11
11
|
constructor();
|
12
|
+
getLocalHost(environmentType?: EnvironmentType): string;
|
12
13
|
_forLocal(port: string | number, path?: string, environmentType?: EnvironmentType): string;
|
13
14
|
_ensureSystem(systemId: string): any;
|
14
15
|
_ensureService(systemId: string, serviceId: string): any;
|
@@ -31,22 +31,22 @@ class ServiceManager {
|
|
31
31
|
});
|
32
32
|
});
|
33
33
|
}
|
34
|
-
|
35
|
-
if (!path) {
|
36
|
-
path = '';
|
37
|
-
}
|
38
|
-
let host;
|
34
|
+
getLocalHost(environmentType) {
|
39
35
|
if (environmentType === 'docker') {
|
40
36
|
//We're inside a docker container, so we can use this special host name to access the host machine
|
41
|
-
|
37
|
+
return 'host.docker.internal';
|
42
38
|
}
|
43
|
-
|
44
|
-
|
39
|
+
return clusterService_1.clusterService.getClusterServiceHost();
|
40
|
+
}
|
41
|
+
_forLocal(port, path, environmentType) {
|
42
|
+
if (!path) {
|
43
|
+
path = '';
|
45
44
|
}
|
45
|
+
const hostname = this.getLocalHost(environmentType);
|
46
46
|
if (path.startsWith('/')) {
|
47
47
|
path = path.substring(1);
|
48
48
|
}
|
49
|
-
return `http://${
|
49
|
+
return `http://${hostname}:${port}/${path}`;
|
50
50
|
}
|
51
51
|
_ensureSystem(systemId) {
|
52
52
|
systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
|
package/dist/cjs/src/types.d.ts
CHANGED
@@ -56,6 +56,30 @@ export type ProcessInfo = {
|
|
56
56
|
pid?: number | string | null;
|
57
57
|
portType?: string;
|
58
58
|
};
|
59
|
+
export interface Health {
|
60
|
+
cmd: string;
|
61
|
+
interval?: number;
|
62
|
+
timeout?: number;
|
63
|
+
retries?: number;
|
64
|
+
}
|
65
|
+
export type PortInfo = {
|
66
|
+
port: number;
|
67
|
+
type: 'tcp' | 'udp';
|
68
|
+
} | number | string;
|
69
|
+
export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
|
70
|
+
image: string;
|
71
|
+
ports: {
|
72
|
+
[key: string]: PortInfo;
|
73
|
+
};
|
74
|
+
credentials?: Credentials;
|
75
|
+
options?: Options;
|
76
|
+
cmd?: string;
|
77
|
+
env?: AnyMap;
|
78
|
+
health?: Health;
|
79
|
+
mounts?: {
|
80
|
+
[key: string]: string;
|
81
|
+
};
|
82
|
+
};
|
59
83
|
export type InstanceInfo = {
|
60
84
|
systemId: string;
|
61
85
|
instanceId: string;
|
@@ -73,6 +97,21 @@ export type InstanceInfo = {
|
|
73
97
|
portType?: string;
|
74
98
|
};
|
75
99
|
export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response, info: ProxyRequestInfo) => void;
|
100
|
+
export interface OperatorInstancePort {
|
101
|
+
protocol: string;
|
102
|
+
port: number;
|
103
|
+
}
|
104
|
+
export interface OperatorInstanceInfo {
|
105
|
+
hostname: string;
|
106
|
+
ports: {
|
107
|
+
[portType: string]: OperatorInstancePort;
|
108
|
+
};
|
109
|
+
path?: string;
|
110
|
+
query?: string;
|
111
|
+
hash?: string;
|
112
|
+
options?: AnyMap;
|
113
|
+
credentials?: AnyMap;
|
114
|
+
}
|
76
115
|
export interface OperatorInfo {
|
77
116
|
host: string;
|
78
117
|
port: string;
|
@@ -288,7 +288,8 @@ class BlockInstanceRunner {
|
|
288
288
|
if (!spec?.local?.image) {
|
289
289
|
throw new Error(`Provider did not have local image: ${providerRef}`);
|
290
290
|
}
|
291
|
-
const
|
291
|
+
const local = spec.local;
|
292
|
+
const dockerImage = local.image;
|
292
293
|
//We only want 1 operator per operator type - across all local systems
|
293
294
|
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
294
295
|
const logs = new LogData_1.LogData();
|
@@ -298,11 +299,13 @@ class BlockInstanceRunner {
|
|
298
299
|
const PortBindings = {};
|
299
300
|
let HealthCheck = undefined;
|
300
301
|
let Mounts = [];
|
301
|
-
const localPorts =
|
302
|
+
const localPorts = local.ports ?? {};
|
303
|
+
const labels = {};
|
302
304
|
const promises = Object.entries(localPorts).map(async ([portType, value]) => {
|
303
|
-
const
|
305
|
+
const portInfo = (0, utils_1.toPortInfo)(value);
|
306
|
+
const dockerPort = `${portInfo.port}/${portInfo.type}`;
|
304
307
|
ExposedPorts[dockerPort] = {};
|
305
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] =
|
308
|
+
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
|
306
309
|
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
307
310
|
PortBindings[dockerPort] = [
|
308
311
|
{
|
@@ -310,19 +313,19 @@ class BlockInstanceRunner {
|
|
310
313
|
HostPort: `${publicPort}`,
|
311
314
|
},
|
312
315
|
];
|
316
|
+
labels[containerManager_1.CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
|
313
317
|
});
|
314
318
|
await Promise.all(promises);
|
315
|
-
if (
|
316
|
-
Object.entries(
|
319
|
+
if (local.env) {
|
320
|
+
Object.entries(local.env).forEach(([key, value]) => {
|
317
321
|
addonEnv[key] = value;
|
318
322
|
});
|
319
323
|
}
|
320
|
-
if (
|
321
|
-
|
322
|
-
Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
|
324
|
+
if (local.mounts) {
|
325
|
+
Mounts = await containerManager_1.containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
|
323
326
|
}
|
324
|
-
if (
|
325
|
-
HealthCheck = containerManager_1.containerManager.toDockerHealth(
|
327
|
+
if (local.health) {
|
328
|
+
HealthCheck = containerManager_1.containerManager.toDockerHealth(local.health);
|
326
329
|
}
|
327
330
|
// For windows we need to default to root
|
328
331
|
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
@@ -342,6 +345,7 @@ class BlockInstanceRunner {
|
|
342
345
|
Mounts,
|
343
346
|
},
|
344
347
|
Labels: {
|
348
|
+
...labels,
|
345
349
|
instance: blockInstance.id,
|
346
350
|
[containerManager_1.COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
|
347
351
|
[containerManager_1.COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
|
@@ -356,7 +360,7 @@ class BlockInstanceRunner {
|
|
356
360
|
}).map(([key, value]) => `${key}=${value}`),
|
357
361
|
],
|
358
362
|
});
|
359
|
-
const portTypes =
|
363
|
+
const portTypes = local.ports ? Object.keys(local.ports) : [];
|
360
364
|
if (portTypes.length > 0) {
|
361
365
|
out.portType = portTypes[0];
|
362
366
|
}
|
@@ -3,8 +3,12 @@
|
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
5
|
import { EntityList } from '@kapeta/schemas';
|
6
|
-
import { AnyMap } from '../types';
|
6
|
+
import { AnyMap, PortInfo } from '../types';
|
7
7
|
export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): string;
|
8
|
+
export declare function toPortInfo(port: PortInfo): {
|
9
|
+
port: number;
|
10
|
+
type: string;
|
11
|
+
};
|
8
12
|
export declare function getRemoteUrl(id: string, defautValue: string): any;
|
9
13
|
export declare function readYML(path: string): any;
|
10
14
|
export declare function isWindows(): boolean;
|
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
8
8
|
};
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
10
|
-
exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.getBlockInstanceContainerName = void 0;
|
10
|
+
exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.toPortInfo = exports.getBlockInstanceContainerName = void 0;
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
12
12
|
const yaml_1 = __importDefault(require("yaml"));
|
13
13
|
const md5_1 = __importDefault(require("md5"));
|
@@ -17,6 +17,16 @@ function getBlockInstanceContainerName(systemId, instanceId) {
|
|
17
17
|
return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
|
18
18
|
}
|
19
19
|
exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
|
20
|
+
function toPortInfo(port) {
|
21
|
+
if (typeof port === 'number' || typeof port === 'string') {
|
22
|
+
return { port: parseInt(`${port}`), type: 'tcp' };
|
23
|
+
}
|
24
|
+
if (!port.type) {
|
25
|
+
port.type = 'tcp';
|
26
|
+
}
|
27
|
+
return port;
|
28
|
+
}
|
29
|
+
exports.toPortInfo = toPortInfo;
|
20
30
|
function getRemoteUrl(id, defautValue) {
|
21
31
|
const remoteConfig = local_cluster_config_1.default.getClusterConfig().remote;
|
22
32
|
return remoteConfig?.[id] ?? defautValue;
|
@@ -148,4 +148,13 @@ router.get('/consumes/resource/:resourceType/:portType/:name', async (req, res)
|
|
148
148
|
router.get('/consumes/:resourceName/:type', (req, res) => {
|
149
149
|
res.send(serviceManager_1.serviceManager.getConsumerAddress(req.kapeta.systemId, req.kapeta.instanceId, req.params.resourceName, req.params.type, req.kapeta.environment));
|
150
150
|
});
|
151
|
+
/**
|
152
|
+
* Used by services to information about a block operator
|
153
|
+
*
|
154
|
+
* If the remote service is not already running it will be started
|
155
|
+
*/
|
156
|
+
router.get('/operator/:instanceId', async (req, res) => {
|
157
|
+
const operatorInfo = await instanceManager_1.instanceManager.getInstanceOperator(req.kapeta.systemId, req.params.instanceId, req.kapeta.environment);
|
158
|
+
res.send(operatorInfo);
|
159
|
+
});
|
151
160
|
exports.default = router;
|
@@ -5,7 +5,7 @@
|
|
5
5
|
/// <reference types="node" />
|
6
6
|
import FSExtra from 'fs-extra';
|
7
7
|
import Docker from 'dockerode';
|
8
|
-
import { InstanceInfo, LogEntry } from './types';
|
8
|
+
import { Health, InstanceInfo, LogEntry } from './types';
|
9
9
|
type StringMap = {
|
10
10
|
[key: string]: string;
|
11
11
|
};
|
@@ -26,12 +26,6 @@ export interface DockerMounts {
|
|
26
26
|
}
|
27
27
|
export type DockerContainerStatus = 'created' | 'running' | 'paused' | 'restarting' | 'removing' | 'exited' | 'dead';
|
28
28
|
export type DockerContainerHealth = 'starting' | 'healthy' | 'unhealthy' | 'none';
|
29
|
-
interface Health {
|
30
|
-
cmd: string;
|
31
|
-
interval?: number;
|
32
|
-
timeout?: number;
|
33
|
-
retries?: number;
|
34
|
-
}
|
35
29
|
export declare const CONTAINER_LABEL_PORT_PREFIX = "kapeta_port-";
|
36
30
|
export declare const COMPOSE_LABEL_PROJECT = "com.docker.compose.project";
|
37
31
|
export declare const COMPOSE_LABEL_SERVICE = "com.docker.compose.service";
|
@@ -50,7 +44,7 @@ declare class ContainerManager {
|
|
50
44
|
isAlive(): boolean;
|
51
45
|
getMountPoint(systemId: string, ref: string, mountName: string): string;
|
52
46
|
createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap>;
|
53
|
-
createVolumes(systemId: string,
|
47
|
+
createVolumes(systemId: string, serviceId: string, mountOpts: StringMap | null | undefined): Promise<DockerMounts[]>;
|
54
48
|
ping(): Promise<void>;
|
55
49
|
docker(): Docker;
|
56
50
|
getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
|
@@ -168,12 +168,12 @@ class ContainerManager {
|
|
168
168
|
}
|
169
169
|
return mounts;
|
170
170
|
}
|
171
|
-
async createVolumes(systemId,
|
171
|
+
async createVolumes(systemId, serviceId, mountOpts) {
|
172
172
|
const Mounts = [];
|
173
173
|
if (mountOpts) {
|
174
174
|
const mountOptList = Object.entries(mountOpts);
|
175
175
|
for (const [mountName, containerPath] of mountOptList) {
|
176
|
-
const volumeName = `${systemId}_${
|
176
|
+
const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
|
177
177
|
Mounts.push({
|
178
178
|
Target: containerPath,
|
179
179
|
Source: volumeName,
|
@@ -182,7 +182,7 @@ class ContainerManager {
|
|
182
182
|
Consistency: 'consistent',
|
183
183
|
Labels: {
|
184
184
|
[exports.COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
|
185
|
-
[exports.COMPOSE_LABEL_SERVICE]:
|
185
|
+
[exports.COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
|
186
186
|
},
|
187
187
|
});
|
188
188
|
}
|
@@ -791,7 +791,7 @@ class ContainerInfo {
|
|
791
791
|
if (!name.startsWith(exports.CONTAINER_LABEL_PORT_PREFIX)) {
|
792
792
|
return;
|
793
793
|
}
|
794
|
-
const hostPort = name.
|
794
|
+
const hostPort = name.substring(exports.CONTAINER_LABEL_PORT_PREFIX.length);
|
795
795
|
portTypes[hostPort] = portType;
|
796
796
|
});
|
797
797
|
lodash_1.default.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
|
@@ -2,7 +2,7 @@
|
|
2
2
|
* Copyright 2023 Kapeta Inc.
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
|
-
import { InstanceInfo, LogEntry } from './types';
|
5
|
+
import { EnvironmentType, InstanceInfo, LogEntry, OperatorInstanceInfo } from './types';
|
6
6
|
import { Task } from './taskManager';
|
7
7
|
export declare class InstanceManager {
|
8
8
|
private _interval;
|
@@ -27,6 +27,7 @@ export declare class InstanceManager {
|
|
27
27
|
stop(systemId: string, instanceId: string): Promise<void>;
|
28
28
|
private stopInner;
|
29
29
|
stopAllForPlan(systemId: string): Task<void>;
|
30
|
+
getInstanceOperator(systemId: string, instanceId: string, environment?: EnvironmentType): Promise<OperatorInstanceInfo>;
|
30
31
|
start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
|
31
32
|
/**
|
32
33
|
* Stops an instance but does not remove it from the list of active instances
|
@@ -323,6 +323,48 @@ class InstanceManager {
|
|
323
323
|
name: `Stopping plan ${systemId}`,
|
324
324
|
});
|
325
325
|
}
|
326
|
+
async getInstanceOperator(systemId, instanceId, environment) {
|
327
|
+
const blockInstance = await assetManager_1.assetManager.getBlockInstance(systemId, instanceId);
|
328
|
+
if (!blockInstance) {
|
329
|
+
throw new Error(`Instance not found: ${systemId}/${instanceId}`);
|
330
|
+
}
|
331
|
+
const blockRef = (0, nodejs_utils_1.normalizeKapetaUri)(blockInstance.block.ref);
|
332
|
+
const block = await assetManager_1.assetManager.getAsset(blockRef, true);
|
333
|
+
if (!block) {
|
334
|
+
throw new Error(`Block not found: ${blockRef}`);
|
335
|
+
}
|
336
|
+
const operatorDefinition = await definitionsManager_1.definitionsManager.getDefinition(block.kind);
|
337
|
+
if (!operatorDefinition?.definition.spec.local) {
|
338
|
+
throw new Error(`Operator block has no local definition: ${blockRef}`);
|
339
|
+
}
|
340
|
+
const localConfig = operatorDefinition.definition.spec.local;
|
341
|
+
let instance = await this.start(systemId, instanceId);
|
342
|
+
if (instance instanceof taskManager_1.Task) {
|
343
|
+
instance = await instance.wait();
|
344
|
+
}
|
345
|
+
const container = await containerManager_1.containerManager.get(instance.pid);
|
346
|
+
if (!container) {
|
347
|
+
throw new Error(`Container not found: ${instance.pid}`);
|
348
|
+
}
|
349
|
+
const portInfo = await container.getPorts();
|
350
|
+
if (!portInfo) {
|
351
|
+
throw new Error(`No ports found for instance: ${instanceId}`);
|
352
|
+
}
|
353
|
+
const hostname = serviceManager_1.serviceManager.getLocalHost(environment);
|
354
|
+
const ports = {};
|
355
|
+
Object.entries(portInfo).forEach(([key, value]) => {
|
356
|
+
ports[key] = {
|
357
|
+
protocol: value.protocol,
|
358
|
+
port: parseInt(value.hostPort),
|
359
|
+
};
|
360
|
+
});
|
361
|
+
return {
|
362
|
+
hostname,
|
363
|
+
ports,
|
364
|
+
credentials: localConfig.credentials,
|
365
|
+
options: localConfig.options,
|
366
|
+
};
|
367
|
+
}
|
326
368
|
async start(systemId, instanceId) {
|
327
369
|
return this.exclusive(systemId, instanceId, async () => {
|
328
370
|
systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
|
@@ -333,7 +375,7 @@ class InstanceManager {
|
|
333
375
|
throw new Error('Block not found: ' + blockRef);
|
334
376
|
}
|
335
377
|
const existingInstance = this.getInstance(systemId, instanceId);
|
336
|
-
if (existingInstance) {
|
378
|
+
if (existingInstance && existingInstance.pid) {
|
337
379
|
if (existingInstance.status === types_1.InstanceStatus.READY) {
|
338
380
|
// Instance is already running
|
339
381
|
return existingInstance;
|
@@ -4,12 +4,12 @@
|
|
4
4
|
*/
|
5
5
|
import { DefinitionInfo } from '@kapeta/local-cluster-config';
|
6
6
|
import { ContainerInfo } from './containerManager';
|
7
|
-
import { EnvironmentType, OperatorInfo } from './types';
|
7
|
+
import { EnvironmentType, LocalImageOptions, OperatorInfo } from './types';
|
8
8
|
export declare const KIND_OPERATOR = "core/resource-type-operator";
|
9
9
|
declare class Operator {
|
10
10
|
private readonly _data;
|
11
11
|
constructor(data: DefinitionInfo);
|
12
|
-
getLocalData():
|
12
|
+
getLocalData(): LocalImageOptions;
|
13
13
|
getDefinitionInfo(): DefinitionInfo;
|
14
14
|
getCredentials(): any;
|
15
15
|
}
|
@@ -137,13 +137,8 @@ class OperatorManager {
|
|
137
137
|
const portType = portTypes[i];
|
138
138
|
let containerPortInfo = operatorData.ports[portType];
|
139
139
|
const hostPort = await serviceManager_1.serviceManager.ensureServicePort(systemId, resourceType, portType);
|
140
|
-
|
141
|
-
|
142
|
-
}
|
143
|
-
if (!containerPortInfo.type) {
|
144
|
-
containerPortInfo.type = 'tcp';
|
145
|
-
}
|
146
|
-
const portId = containerPortInfo.port + '/' + containerPortInfo.type;
|
140
|
+
const portInfo = (0, utils_1.toPortInfo)(containerPortInfo);
|
141
|
+
const portId = portInfo.port + '/' + portInfo.type;
|
147
142
|
ports[portId] = {
|
148
143
|
type: portType,
|
149
144
|
hostPort,
|
@@ -9,6 +9,7 @@ export declare const HTTP_PORTS: string[];
|
|
9
9
|
declare class ServiceManager {
|
10
10
|
private _systems;
|
11
11
|
constructor();
|
12
|
+
getLocalHost(environmentType?: EnvironmentType): string;
|
12
13
|
_forLocal(port: string | number, path?: string, environmentType?: EnvironmentType): string;
|
13
14
|
_ensureSystem(systemId: string): any;
|
14
15
|
_ensureService(systemId: string, serviceId: string): any;
|
@@ -31,22 +31,22 @@ class ServiceManager {
|
|
31
31
|
});
|
32
32
|
});
|
33
33
|
}
|
34
|
-
|
35
|
-
if (!path) {
|
36
|
-
path = '';
|
37
|
-
}
|
38
|
-
let host;
|
34
|
+
getLocalHost(environmentType) {
|
39
35
|
if (environmentType === 'docker') {
|
40
36
|
//We're inside a docker container, so we can use this special host name to access the host machine
|
41
|
-
|
37
|
+
return 'host.docker.internal';
|
42
38
|
}
|
43
|
-
|
44
|
-
|
39
|
+
return clusterService_1.clusterService.getClusterServiceHost();
|
40
|
+
}
|
41
|
+
_forLocal(port, path, environmentType) {
|
42
|
+
if (!path) {
|
43
|
+
path = '';
|
45
44
|
}
|
45
|
+
const hostname = this.getLocalHost(environmentType);
|
46
46
|
if (path.startsWith('/')) {
|
47
47
|
path = path.substring(1);
|
48
48
|
}
|
49
|
-
return `http://${
|
49
|
+
return `http://${hostname}:${port}/${path}`;
|
50
50
|
}
|
51
51
|
_ensureSystem(systemId) {
|
52
52
|
systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
|
package/dist/esm/src/types.d.ts
CHANGED
@@ -56,6 +56,30 @@ export type ProcessInfo = {
|
|
56
56
|
pid?: number | string | null;
|
57
57
|
portType?: string;
|
58
58
|
};
|
59
|
+
export interface Health {
|
60
|
+
cmd: string;
|
61
|
+
interval?: number;
|
62
|
+
timeout?: number;
|
63
|
+
retries?: number;
|
64
|
+
}
|
65
|
+
export type PortInfo = {
|
66
|
+
port: number;
|
67
|
+
type: 'tcp' | 'udp';
|
68
|
+
} | number | string;
|
69
|
+
export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
|
70
|
+
image: string;
|
71
|
+
ports: {
|
72
|
+
[key: string]: PortInfo;
|
73
|
+
};
|
74
|
+
credentials?: Credentials;
|
75
|
+
options?: Options;
|
76
|
+
cmd?: string;
|
77
|
+
env?: AnyMap;
|
78
|
+
health?: Health;
|
79
|
+
mounts?: {
|
80
|
+
[key: string]: string;
|
81
|
+
};
|
82
|
+
};
|
59
83
|
export type InstanceInfo = {
|
60
84
|
systemId: string;
|
61
85
|
instanceId: string;
|
@@ -73,6 +97,21 @@ export type InstanceInfo = {
|
|
73
97
|
portType?: string;
|
74
98
|
};
|
75
99
|
export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response, info: ProxyRequestInfo) => void;
|
100
|
+
export interface OperatorInstancePort {
|
101
|
+
protocol: string;
|
102
|
+
port: number;
|
103
|
+
}
|
104
|
+
export interface OperatorInstanceInfo {
|
105
|
+
hostname: string;
|
106
|
+
ports: {
|
107
|
+
[portType: string]: OperatorInstancePort;
|
108
|
+
};
|
109
|
+
path?: string;
|
110
|
+
query?: string;
|
111
|
+
hash?: string;
|
112
|
+
options?: AnyMap;
|
113
|
+
credentials?: AnyMap;
|
114
|
+
}
|
76
115
|
export interface OperatorInfo {
|
77
116
|
host: string;
|
78
117
|
port: string;
|
@@ -288,7 +288,8 @@ class BlockInstanceRunner {
|
|
288
288
|
if (!spec?.local?.image) {
|
289
289
|
throw new Error(`Provider did not have local image: ${providerRef}`);
|
290
290
|
}
|
291
|
-
const
|
291
|
+
const local = spec.local;
|
292
|
+
const dockerImage = local.image;
|
292
293
|
//We only want 1 operator per operator type - across all local systems
|
293
294
|
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
294
295
|
const logs = new LogData_1.LogData();
|
@@ -298,11 +299,13 @@ class BlockInstanceRunner {
|
|
298
299
|
const PortBindings = {};
|
299
300
|
let HealthCheck = undefined;
|
300
301
|
let Mounts = [];
|
301
|
-
const localPorts =
|
302
|
+
const localPorts = local.ports ?? {};
|
303
|
+
const labels = {};
|
302
304
|
const promises = Object.entries(localPorts).map(async ([portType, value]) => {
|
303
|
-
const
|
305
|
+
const portInfo = (0, utils_1.toPortInfo)(value);
|
306
|
+
const dockerPort = `${portInfo.port}/${portInfo.type}`;
|
304
307
|
ExposedPorts[dockerPort] = {};
|
305
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] =
|
308
|
+
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
|
306
309
|
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
307
310
|
PortBindings[dockerPort] = [
|
308
311
|
{
|
@@ -310,19 +313,19 @@ class BlockInstanceRunner {
|
|
310
313
|
HostPort: `${publicPort}`,
|
311
314
|
},
|
312
315
|
];
|
316
|
+
labels[containerManager_1.CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
|
313
317
|
});
|
314
318
|
await Promise.all(promises);
|
315
|
-
if (
|
316
|
-
Object.entries(
|
319
|
+
if (local.env) {
|
320
|
+
Object.entries(local.env).forEach(([key, value]) => {
|
317
321
|
addonEnv[key] = value;
|
318
322
|
});
|
319
323
|
}
|
320
|
-
if (
|
321
|
-
|
322
|
-
Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
|
324
|
+
if (local.mounts) {
|
325
|
+
Mounts = await containerManager_1.containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
|
323
326
|
}
|
324
|
-
if (
|
325
|
-
HealthCheck = containerManager_1.containerManager.toDockerHealth(
|
327
|
+
if (local.health) {
|
328
|
+
HealthCheck = containerManager_1.containerManager.toDockerHealth(local.health);
|
326
329
|
}
|
327
330
|
// For windows we need to default to root
|
328
331
|
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
@@ -342,6 +345,7 @@ class BlockInstanceRunner {
|
|
342
345
|
Mounts,
|
343
346
|
},
|
344
347
|
Labels: {
|
348
|
+
...labels,
|
345
349
|
instance: blockInstance.id,
|
346
350
|
[containerManager_1.COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
|
347
351
|
[containerManager_1.COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
|
@@ -356,7 +360,7 @@ class BlockInstanceRunner {
|
|
356
360
|
}).map(([key, value]) => `${key}=${value}`),
|
357
361
|
],
|
358
362
|
});
|
359
|
-
const portTypes =
|
363
|
+
const portTypes = local.ports ? Object.keys(local.ports) : [];
|
360
364
|
if (portTypes.length > 0) {
|
361
365
|
out.portType = portTypes[0];
|
362
366
|
}
|
@@ -3,8 +3,12 @@
|
|
3
3
|
* SPDX-License-Identifier: BUSL-1.1
|
4
4
|
*/
|
5
5
|
import { EntityList } from '@kapeta/schemas';
|
6
|
-
import { AnyMap } from '../types';
|
6
|
+
import { AnyMap, PortInfo } from '../types';
|
7
7
|
export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): string;
|
8
|
+
export declare function toPortInfo(port: PortInfo): {
|
9
|
+
port: number;
|
10
|
+
type: string;
|
11
|
+
};
|
8
12
|
export declare function getRemoteUrl(id: string, defautValue: string): any;
|
9
13
|
export declare function readYML(path: string): any;
|
10
14
|
export declare function isWindows(): boolean;
|
@@ -7,7 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
8
8
|
};
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
10
|
-
exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.getBlockInstanceContainerName = void 0;
|
10
|
+
exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.toPortInfo = exports.getBlockInstanceContainerName = void 0;
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
12
12
|
const yaml_1 = __importDefault(require("yaml"));
|
13
13
|
const md5_1 = __importDefault(require("md5"));
|
@@ -17,6 +17,16 @@ function getBlockInstanceContainerName(systemId, instanceId) {
|
|
17
17
|
return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
|
18
18
|
}
|
19
19
|
exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
|
20
|
+
function toPortInfo(port) {
|
21
|
+
if (typeof port === 'number' || typeof port === 'string') {
|
22
|
+
return { port: parseInt(`${port}`), type: 'tcp' };
|
23
|
+
}
|
24
|
+
if (!port.type) {
|
25
|
+
port.type = 'tcp';
|
26
|
+
}
|
27
|
+
return port;
|
28
|
+
}
|
29
|
+
exports.toPortInfo = toPortInfo;
|
20
30
|
function getRemoteUrl(id, defautValue) {
|
21
31
|
const remoteConfig = local_cluster_config_1.default.getClusterConfig().remote;
|
22
32
|
return remoteConfig?.[id] ?? defautValue;
|
package/package.json
CHANGED
package/src/config/routes.ts
CHANGED
@@ -181,4 +181,19 @@ router.get('/consumes/:resourceName/:type', (req: KapetaRequest, res) => {
|
|
181
181
|
);
|
182
182
|
});
|
183
183
|
|
184
|
+
/**
|
185
|
+
* Used by services to information about a block operator
|
186
|
+
*
|
187
|
+
* If the remote service is not already running it will be started
|
188
|
+
*/
|
189
|
+
router.get('/operator/:instanceId', async (req: KapetaRequest, res) => {
|
190
|
+
const operatorInfo = await instanceManager.getInstanceOperator(
|
191
|
+
req.kapeta!.systemId,
|
192
|
+
req.params.instanceId,
|
193
|
+
req.kapeta!.environment
|
194
|
+
);
|
195
|
+
|
196
|
+
res.send(operatorInfo);
|
197
|
+
});
|
198
|
+
|
184
199
|
export default router;
|
package/src/containerManager.ts
CHANGED
@@ -14,7 +14,7 @@ import ClusterConfiguration from '@kapeta/local-cluster-config';
|
|
14
14
|
import uuid from 'node-uuid';
|
15
15
|
import md5 from 'md5';
|
16
16
|
import { getBlockInstanceContainerName } from './utils/utils';
|
17
|
-
import { InstanceInfo, LogEntry, LogSource } from './types';
|
17
|
+
import { Health, InstanceInfo, LogEntry, LogSource } from './types';
|
18
18
|
import { KapetaAPI } from '@kapeta/nodejs-api-client';
|
19
19
|
import { taskManager, Task } from './taskManager';
|
20
20
|
import { EventEmitter } from 'node:events';
|
@@ -76,13 +76,6 @@ interface JSONMessage<T = string> {
|
|
76
76
|
aux?: any;
|
77
77
|
}
|
78
78
|
|
79
|
-
interface Health {
|
80
|
-
cmd: string;
|
81
|
-
interval?: number;
|
82
|
-
timeout?: number;
|
83
|
-
retries?: number;
|
84
|
-
}
|
85
|
-
|
86
79
|
export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
|
87
80
|
const NANO_SECOND = 1000000;
|
88
81
|
const HEALTH_CHECK_INTERVAL = 3000;
|
@@ -255,7 +248,7 @@ class ContainerManager {
|
|
255
248
|
|
256
249
|
async createVolumes(
|
257
250
|
systemId: string,
|
258
|
-
|
251
|
+
serviceId: string,
|
259
252
|
mountOpts: StringMap | null | undefined
|
260
253
|
): Promise<DockerMounts[]> {
|
261
254
|
const Mounts: DockerMounts[] = [];
|
@@ -263,7 +256,7 @@ class ContainerManager {
|
|
263
256
|
if (mountOpts) {
|
264
257
|
const mountOptList = Object.entries(mountOpts);
|
265
258
|
for (const [mountName, containerPath] of mountOptList) {
|
266
|
-
const volumeName = `${systemId}_${
|
259
|
+
const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
|
267
260
|
|
268
261
|
Mounts.push({
|
269
262
|
Target: containerPath,
|
@@ -273,7 +266,7 @@ class ContainerManager {
|
|
273
266
|
Consistency: 'consistent',
|
274
267
|
Labels: {
|
275
268
|
[COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
|
276
|
-
[COMPOSE_LABEL_SERVICE]:
|
269
|
+
[COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
|
277
270
|
},
|
278
271
|
});
|
279
272
|
}
|
@@ -997,7 +990,7 @@ export class ContainerInfo {
|
|
997
990
|
return;
|
998
991
|
}
|
999
992
|
|
1000
|
-
const hostPort = name.
|
993
|
+
const hostPort = name.substring(CONTAINER_LABEL_PORT_PREFIX.length);
|
1001
994
|
|
1002
995
|
portTypes[hostPort] = portType;
|
1003
996
|
});
|
package/src/instanceManager.ts
CHANGED
@@ -18,7 +18,19 @@ import {
|
|
18
18
|
HEALTH_CHECK_TIMEOUT,
|
19
19
|
} from './containerManager';
|
20
20
|
import { configManager } from './configManager';
|
21
|
-
import {
|
21
|
+
import {
|
22
|
+
DesiredInstanceStatus,
|
23
|
+
EnvironmentType,
|
24
|
+
InstanceInfo,
|
25
|
+
InstanceOwner,
|
26
|
+
InstanceStatus,
|
27
|
+
InstanceType,
|
28
|
+
LocalImageOptions,
|
29
|
+
LogEntry,
|
30
|
+
OperatorInfo,
|
31
|
+
OperatorInstanceInfo,
|
32
|
+
OperatorInstancePort,
|
33
|
+
} from './types';
|
22
34
|
import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
|
23
35
|
import { getBlockInstanceContainerName, getResolvedConfiguration } from './utils/utils';
|
24
36
|
import { KIND_OPERATOR, operatorManager } from './operatorManager';
|
@@ -414,6 +426,62 @@ export class InstanceManager {
|
|
414
426
|
);
|
415
427
|
}
|
416
428
|
|
429
|
+
public async getInstanceOperator(
|
430
|
+
systemId: string,
|
431
|
+
instanceId: string,
|
432
|
+
environment?: EnvironmentType
|
433
|
+
): Promise<OperatorInstanceInfo> {
|
434
|
+
const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
|
435
|
+
if (!blockInstance) {
|
436
|
+
throw new Error(`Instance not found: ${systemId}/${instanceId}`);
|
437
|
+
}
|
438
|
+
const blockRef = normalizeKapetaUri(blockInstance.block.ref);
|
439
|
+
const block = await assetManager.getAsset(blockRef, true);
|
440
|
+
if (!block) {
|
441
|
+
throw new Error(`Block not found: ${blockRef}`);
|
442
|
+
}
|
443
|
+
|
444
|
+
const operatorDefinition = await definitionsManager.getDefinition(block.kind);
|
445
|
+
|
446
|
+
if (!operatorDefinition?.definition.spec.local) {
|
447
|
+
throw new Error(`Operator block has no local definition: ${blockRef}`);
|
448
|
+
}
|
449
|
+
|
450
|
+
const localConfig = operatorDefinition.definition.spec.local as LocalImageOptions;
|
451
|
+
|
452
|
+
let instance = await this.start(systemId, instanceId);
|
453
|
+
if (instance instanceof Task) {
|
454
|
+
instance = await instance.wait();
|
455
|
+
}
|
456
|
+
|
457
|
+
const container = await containerManager.get(instance.pid as string);
|
458
|
+
if (!container) {
|
459
|
+
throw new Error(`Container not found: ${instance.pid}`);
|
460
|
+
}
|
461
|
+
|
462
|
+
const portInfo = await container.getPorts();
|
463
|
+
if (!portInfo) {
|
464
|
+
throw new Error(`No ports found for instance: ${instanceId}`);
|
465
|
+
}
|
466
|
+
|
467
|
+
const hostname = serviceManager.getLocalHost(environment);
|
468
|
+
const ports: { [key: string]: OperatorInstancePort } = {};
|
469
|
+
|
470
|
+
Object.entries(portInfo).forEach(([key, value]) => {
|
471
|
+
ports[key] = {
|
472
|
+
protocol: value.protocol,
|
473
|
+
port: parseInt(value.hostPort),
|
474
|
+
};
|
475
|
+
});
|
476
|
+
|
477
|
+
return {
|
478
|
+
hostname,
|
479
|
+
ports,
|
480
|
+
credentials: localConfig.credentials,
|
481
|
+
options: localConfig.options,
|
482
|
+
};
|
483
|
+
}
|
484
|
+
|
417
485
|
public async start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>> {
|
418
486
|
return this.exclusive(systemId, instanceId, async () => {
|
419
487
|
systemId = normalizeKapetaUri(systemId);
|
@@ -427,7 +495,7 @@ export class InstanceManager {
|
|
427
495
|
|
428
496
|
const existingInstance = this.getInstance(systemId, instanceId);
|
429
497
|
|
430
|
-
if (existingInstance) {
|
498
|
+
if (existingInstance && existingInstance.pid) {
|
431
499
|
if (existingInstance.status === InstanceStatus.READY) {
|
432
500
|
// Instance is already running
|
433
501
|
return existingInstance;
|
@@ -481,8 +549,8 @@ export class InstanceManager {
|
|
481
549
|
return Promise.resolve();
|
482
550
|
}
|
483
551
|
// Check if the operator has a local definition, if not we skip it since we can't start it
|
484
|
-
if(!asset.definition.spec.local) {
|
485
|
-
console.log('Skipping operator since it as no local definition: %s', consumer.kind)
|
552
|
+
if (!asset.definition.spec.local) {
|
553
|
+
console.log('Skipping operator since it as no local definition: %s', consumer.kind);
|
486
554
|
return Promise.resolve();
|
487
555
|
}
|
488
556
|
console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
|
package/src/operatorManager.ts
CHANGED
@@ -16,10 +16,10 @@ import {
|
|
16
16
|
containerManager,
|
17
17
|
} from './containerManager';
|
18
18
|
import FSExtra from 'fs-extra';
|
19
|
-
import { AnyMap, EnvironmentType, OperatorInfo, StringMap } from './types';
|
19
|
+
import { AnyMap, EnvironmentType, LocalImageOptions, OperatorInfo, StringMap } from './types';
|
20
20
|
import { BlockInstance, Resource } from '@kapeta/schemas';
|
21
21
|
import { definitionsManager } from './definitionsManager';
|
22
|
-
import { getBindHost } from './utils/utils';
|
22
|
+
import { getBindHost, toPortInfo } from './utils/utils';
|
23
23
|
import { parseKapetaUri, normalizeKapetaUri } from '@kapeta/nodejs-utils';
|
24
24
|
import _ from 'lodash';
|
25
25
|
import AsyncLock from 'async-lock';
|
@@ -34,7 +34,7 @@ class Operator {
|
|
34
34
|
this._data = data;
|
35
35
|
}
|
36
36
|
|
37
|
-
getLocalData() {
|
37
|
+
getLocalData(): LocalImageOptions {
|
38
38
|
return this._data.definition.spec.local;
|
39
39
|
}
|
40
40
|
|
@@ -187,16 +187,8 @@ class OperatorManager {
|
|
187
187
|
const portType = portTypes[i];
|
188
188
|
let containerPortInfo = operatorData.ports[portType];
|
189
189
|
const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
|
190
|
-
|
191
|
-
|
192
|
-
containerPortInfo = { port: containerPortInfo, type: 'tcp' };
|
193
|
-
}
|
194
|
-
|
195
|
-
if (!containerPortInfo.type) {
|
196
|
-
containerPortInfo.type = 'tcp';
|
197
|
-
}
|
198
|
-
|
199
|
-
const portId = containerPortInfo.port + '/' + containerPortInfo.type;
|
190
|
+
const portInfo = toPortInfo(containerPortInfo);
|
191
|
+
const portId = portInfo.port + '/' + portInfo.type;
|
200
192
|
|
201
193
|
ports[portId] = {
|
202
194
|
type: portType,
|
package/src/serviceManager.ts
CHANGED
@@ -34,22 +34,25 @@ class ServiceManager {
|
|
34
34
|
});
|
35
35
|
}
|
36
36
|
|
37
|
+
public getLocalHost(environmentType?: EnvironmentType) {
|
38
|
+
if (environmentType === 'docker') {
|
39
|
+
//We're inside a docker container, so we can use this special host name to access the host machine
|
40
|
+
return 'host.docker.internal';
|
41
|
+
}
|
42
|
+
|
43
|
+
return clusterService.getClusterServiceHost();
|
44
|
+
}
|
45
|
+
|
37
46
|
_forLocal(port: string | number, path?: string, environmentType?: EnvironmentType) {
|
38
47
|
if (!path) {
|
39
48
|
path = '';
|
40
49
|
}
|
41
|
-
|
42
|
-
if (environmentType === 'docker') {
|
43
|
-
//We're inside a docker container, so we can use this special host name to access the host machine
|
44
|
-
host = 'host.docker.internal';
|
45
|
-
} else {
|
46
|
-
host = clusterService.getClusterServiceHost();
|
47
|
-
}
|
50
|
+
const hostname = this.getLocalHost(environmentType);
|
48
51
|
|
49
52
|
if (path.startsWith('/')) {
|
50
53
|
path = path.substring(1);
|
51
54
|
}
|
52
|
-
return `http://${
|
55
|
+
return `http://${hostname}:${port}/${path}`;
|
53
56
|
}
|
54
57
|
|
55
58
|
_ensureSystem(systemId: string) {
|
package/src/types.ts
CHANGED
@@ -61,6 +61,26 @@ export type ProcessInfo = {
|
|
61
61
|
portType?: string;
|
62
62
|
};
|
63
63
|
|
64
|
+
export interface Health {
|
65
|
+
cmd: string;
|
66
|
+
interval?: number;
|
67
|
+
timeout?: number;
|
68
|
+
retries?: number;
|
69
|
+
}
|
70
|
+
|
71
|
+
export type PortInfo = { port: number; type: 'tcp' | 'udp' } | number | string;
|
72
|
+
|
73
|
+
export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
|
74
|
+
image: string;
|
75
|
+
ports: { [key: string]: PortInfo };
|
76
|
+
credentials?: Credentials;
|
77
|
+
options?: Options;
|
78
|
+
cmd?: string;
|
79
|
+
env?: AnyMap;
|
80
|
+
health?: Health;
|
81
|
+
mounts?: { [key: string]: string };
|
82
|
+
};
|
83
|
+
|
64
84
|
export type InstanceInfo = {
|
65
85
|
systemId: string;
|
66
86
|
instanceId: string;
|
@@ -86,6 +106,21 @@ interface ResourceRef {
|
|
86
106
|
|
87
107
|
export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response, info: ProxyRequestInfo) => void;
|
88
108
|
|
109
|
+
export interface OperatorInstancePort {
|
110
|
+
protocol: string;
|
111
|
+
port: number;
|
112
|
+
}
|
113
|
+
|
114
|
+
export interface OperatorInstanceInfo {
|
115
|
+
hostname: string;
|
116
|
+
ports: { [portType: string]: OperatorInstancePort };
|
117
|
+
path?: string;
|
118
|
+
query?: string;
|
119
|
+
hash?: string;
|
120
|
+
options?: AnyMap;
|
121
|
+
credentials?: AnyMap;
|
122
|
+
}
|
123
|
+
|
89
124
|
export interface OperatorInfo {
|
90
125
|
host: string;
|
91
126
|
port: string;
|
@@ -5,19 +5,20 @@
|
|
5
5
|
|
6
6
|
import FSExtra from 'fs-extra';
|
7
7
|
import ClusterConfig, { DefinitionInfo } from '@kapeta/local-cluster-config';
|
8
|
-
import { getBindHost, getBlockInstanceContainerName, readYML } from './utils';
|
8
|
+
import { getBindHost, getBlockInstanceContainerName, readYML, toPortInfo } from './utils';
|
9
9
|
import { KapetaURI, parseKapetaUri, normalizeKapetaUri } from '@kapeta/nodejs-utils';
|
10
10
|
import { DEFAULT_PORT_TYPE, HTTP_PORT_TYPE, HTTP_PORTS, serviceManager } from '../serviceManager';
|
11
11
|
import {
|
12
12
|
COMPOSE_LABEL_PROJECT,
|
13
13
|
COMPOSE_LABEL_SERVICE,
|
14
|
+
CONTAINER_LABEL_PORT_PREFIX,
|
14
15
|
containerManager,
|
15
16
|
DockerMounts,
|
16
17
|
toLocalBindVolume,
|
17
18
|
} from '../containerManager';
|
18
19
|
import { LogData } from './LogData';
|
19
20
|
import { clusterService } from '../clusterService';
|
20
|
-
import { AnyMap, BlockProcessParams, InstanceType, ProcessInfo, StringMap } from '../types';
|
21
|
+
import { AnyMap, BlockProcessParams, InstanceType, LocalImageOptions, ProcessInfo, StringMap } from '../types';
|
21
22
|
import { definitionsManager } from '../definitionsManager';
|
22
23
|
import Docker from 'dockerode';
|
23
24
|
import OS from 'node:os';
|
@@ -384,7 +385,9 @@ export class BlockInstanceRunner {
|
|
384
385
|
throw new Error(`Provider did not have local image: ${providerRef}`);
|
385
386
|
}
|
386
387
|
|
387
|
-
const
|
388
|
+
const local = spec.local as LocalImageOptions;
|
389
|
+
|
390
|
+
const dockerImage = local.image;
|
388
391
|
|
389
392
|
//We only want 1 operator per operator type - across all local systems
|
390
393
|
const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
|
@@ -397,11 +400,13 @@ export class BlockInstanceRunner {
|
|
397
400
|
const PortBindings: AnyMap = {};
|
398
401
|
let HealthCheck = undefined;
|
399
402
|
let Mounts: DockerMounts[] = [];
|
400
|
-
const localPorts =
|
403
|
+
const localPorts = local.ports ?? {};
|
404
|
+
const labels: { [key: string]: string } = {};
|
401
405
|
const promises = Object.entries(localPorts).map(async ([portType, value]) => {
|
402
|
-
const
|
406
|
+
const portInfo = toPortInfo(value);
|
407
|
+
const dockerPort = `${portInfo.port}/${portInfo.type}`;
|
403
408
|
ExposedPorts[dockerPort] = {};
|
404
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] =
|
409
|
+
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
|
405
410
|
const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
406
411
|
PortBindings[dockerPort] = [
|
407
412
|
{
|
@@ -409,23 +414,24 @@ export class BlockInstanceRunner {
|
|
409
414
|
HostPort: `${publicPort}`,
|
410
415
|
},
|
411
416
|
];
|
417
|
+
|
418
|
+
labels[CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
|
412
419
|
});
|
413
420
|
|
414
421
|
await Promise.all(promises);
|
415
422
|
|
416
|
-
if (
|
417
|
-
Object.entries(
|
423
|
+
if (local.env) {
|
424
|
+
Object.entries(local.env).forEach(([key, value]) => {
|
418
425
|
addonEnv[key] = value as string;
|
419
426
|
});
|
420
427
|
}
|
421
428
|
|
422
|
-
if (
|
423
|
-
|
424
|
-
Mounts = containerManager.toDockerMounts(mounts);
|
429
|
+
if (local.mounts) {
|
430
|
+
Mounts = await containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
|
425
431
|
}
|
426
432
|
|
427
|
-
if (
|
428
|
-
HealthCheck = containerManager.toDockerHealth(
|
433
|
+
if (local.health) {
|
434
|
+
HealthCheck = containerManager.toDockerHealth(local.health);
|
429
435
|
}
|
430
436
|
|
431
437
|
// For windows we need to default to root
|
@@ -447,6 +453,7 @@ export class BlockInstanceRunner {
|
|
447
453
|
Mounts,
|
448
454
|
},
|
449
455
|
Labels: {
|
456
|
+
...labels,
|
450
457
|
instance: blockInstance.id,
|
451
458
|
[COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
|
452
459
|
[COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
|
@@ -462,7 +469,7 @@ export class BlockInstanceRunner {
|
|
462
469
|
],
|
463
470
|
});
|
464
471
|
|
465
|
-
const portTypes =
|
472
|
+
const portTypes = local.ports ? Object.keys(local.ports) : [];
|
466
473
|
if (portTypes.length > 0) {
|
467
474
|
out.portType = portTypes[0];
|
468
475
|
}
|
package/src/utils/utils.ts
CHANGED
@@ -5,17 +5,28 @@
|
|
5
5
|
|
6
6
|
import FS from 'node:fs';
|
7
7
|
import YAML from 'yaml';
|
8
|
-
import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
9
8
|
import md5 from 'md5';
|
10
9
|
import { EntityList } from '@kapeta/schemas';
|
11
10
|
import _ from 'lodash';
|
12
|
-
import { AnyMap } from '../types';
|
11
|
+
import { AnyMap, PortInfo } from '../types';
|
13
12
|
import ClusterConfiguration from '@kapeta/local-cluster-config';
|
14
13
|
|
15
14
|
export function getBlockInstanceContainerName(systemId: string, instanceId: string) {
|
16
15
|
return `kapeta-block-instance-${md5(systemId + instanceId)}`;
|
17
16
|
}
|
18
17
|
|
18
|
+
export function toPortInfo(port: PortInfo) {
|
19
|
+
if (typeof port === 'number' || typeof port === 'string') {
|
20
|
+
return { port: parseInt(`${port}`), type: 'tcp' };
|
21
|
+
}
|
22
|
+
|
23
|
+
if (!port.type) {
|
24
|
+
port.type = 'tcp';
|
25
|
+
}
|
26
|
+
|
27
|
+
return port;
|
28
|
+
}
|
29
|
+
|
19
30
|
export function getRemoteUrl(id: string, defautValue: string) {
|
20
31
|
const remoteConfig = ClusterConfiguration.getClusterConfig().remote;
|
21
32
|
return remoteConfig?.[id] ?? defautValue;
|