@docker-harpoon/core 0.1.4 → 0.2.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/dist/api/promise.d.ts +1 -1
- package/dist/api/promise.d.ts.map +1 -1
- package/dist/api/promise.js +28 -18
- package/dist/bindings/index.d.ts +2 -2
- package/dist/bindings/index.d.ts.map +1 -1
- package/dist/bindings/index.js +1 -1
- package/dist/bindings/types.d.ts.map +1 -1
- package/dist/bindings/types.js +1 -3
- package/dist/build-strategies/types.d.ts.map +1 -1
- package/dist/config-patchers/index.d.ts.map +1 -1
- package/dist/config-patchers/types.d.ts.map +1 -1
- package/dist/dockerfile-transformers/core.d.ts.map +1 -1
- package/dist/dockerfile-transformers/core.js +2 -5
- package/dist/dockerfile-transformers/index.d.ts.map +1 -1
- package/dist/dockerfile-transformers/types.d.ts.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -3
- package/dist/helpers/database.d.ts.map +1 -1
- package/dist/helpers/database.js +1 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/resources/container.d.ts +7 -2
- package/dist/resources/container.d.ts.map +1 -1
- package/dist/resources/container.js +222 -31
- package/dist/resources/image.d.ts.map +1 -1
- package/dist/resources/image.js +14 -35
- package/dist/resources/network.d.ts.map +1 -1
- package/dist/resources/network.js +23 -9
- package/dist/resources/schemas.d.ts +177 -18
- package/dist/resources/schemas.d.ts.map +1 -1
- package/dist/resources/schemas.js +1 -1
- package/dist/services/CircuitBreaker.d.ts +83 -0
- package/dist/services/CircuitBreaker.d.ts.map +1 -0
- package/dist/services/CircuitBreaker.js +164 -0
- package/dist/services/ContainerPool.d.ts +82 -0
- package/dist/services/ContainerPool.d.ts.map +1 -0
- package/dist/services/ContainerPool.js +186 -0
- package/dist/services/DockerBatcher.d.ts +74 -0
- package/dist/services/DockerBatcher.d.ts.map +1 -0
- package/dist/services/DockerBatcher.js +107 -0
- package/dist/services/DockerClient.d.ts +125 -0
- package/dist/services/DockerClient.d.ts.map +1 -0
- package/dist/services/DockerClient.js +220 -0
- package/dist/services/DockerErrors.d.ts +145 -0
- package/dist/services/DockerErrors.d.ts.map +1 -0
- package/dist/services/DockerErrors.js +224 -0
- package/dist/services/DockerRateLimiter.d.ts +80 -0
- package/dist/services/DockerRateLimiter.d.ts.map +1 -0
- package/dist/services/DockerRateLimiter.js +93 -0
- package/dist/services/EventBus.d.ts +126 -0
- package/dist/services/EventBus.d.ts.map +1 -0
- package/dist/services/EventBus.js +111 -0
- package/dist/services/Harpoon.d.ts +151 -0
- package/dist/services/Harpoon.d.ts.map +1 -0
- package/dist/services/Harpoon.js +148 -0
- package/dist/services/HarpoonConfig.d.ts +60 -0
- package/dist/services/HarpoonConfig.d.ts.map +1 -0
- package/dist/services/HarpoonConfig.js +67 -0
- package/dist/services/HarpoonLogger.d.ts +36 -0
- package/dist/services/HarpoonLogger.d.ts.map +1 -0
- package/dist/services/HarpoonLogger.js +94 -0
- package/dist/services/ReadinessCoordinator.d.ts +128 -0
- package/dist/services/ReadinessCoordinator.d.ts.map +1 -0
- package/dist/services/ReadinessCoordinator.js +170 -0
- package/dist/services/ResourceTracker.d.ts +74 -0
- package/dist/services/ResourceTracker.d.ts.map +1 -0
- package/dist/services/ResourceTracker.js +145 -0
- package/dist/services/index.d.ts +29 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +47 -0
- package/dist/testing/helpers.d.ts +114 -0
- package/dist/testing/helpers.d.ts.map +1 -0
- package/dist/testing/helpers.js +140 -0
- package/dist/testing/index.d.ts +29 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +47 -0
- package/dist/testing/mocks.d.ts +66 -0
- package/dist/testing/mocks.d.ts.map +1 -0
- package/dist/testing/mocks.js +224 -0
- package/dist/utils/process.d.ts +24 -0
- package/dist/utils/process.d.ts.map +1 -0
- package/dist/utils/process.js +49 -0
- package/package.json +12 -8
- package/dist/__tests__/bindings.test.d.ts +0 -10
- package/dist/__tests__/bindings.test.d.ts.map +0 -1
- package/dist/__tests__/bindings.test.js +0 -128
- package/dist/__tests__/container.test.d.ts +0 -12
- package/dist/__tests__/container.test.d.ts.map +0 -1
- package/dist/__tests__/container.test.js +0 -136
- package/dist/__tests__/database.test.d.ts +0 -10
- package/dist/__tests__/database.test.d.ts.map +0 -1
- package/dist/__tests__/database.test.js +0 -78
- package/dist/__tests__/docker-infra.template.d.ts +0 -2
- package/dist/__tests__/docker-infra.template.d.ts.map +0 -1
- package/dist/__tests__/docker-infra.template.js +0 -174
- package/dist/__tests__/network.test.d.ts +0 -10
- package/dist/__tests__/network.test.d.ts.map +0 -1
- package/dist/__tests__/network.test.js +0 -44
- package/dist/__tests__/test-setup.d.ts +0 -9
- package/dist/__tests__/test-setup.d.ts.map +0 -1
- package/dist/__tests__/test-setup.js +0 -20
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Implementations for Testing
|
|
3
|
+
*
|
|
4
|
+
* Provides mock implementations of Harpoon services for unit testing.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect, Layer, Ref, PubSub, Stream, Duration, Option } from 'effect';
|
|
7
|
+
import { DockerClient, } from '../services/DockerClient';
|
|
8
|
+
import { DockerRateLimiter } from '../services/DockerRateLimiter';
|
|
9
|
+
import { CircuitBreaker } from '../services/CircuitBreaker';
|
|
10
|
+
import { ResourceTracker, } from '../services/ResourceTracker';
|
|
11
|
+
import '../services/ReadinessCoordinator';
|
|
12
|
+
import { HarpoonConfig } from '../services/HarpoonConfig';
|
|
13
|
+
/**
|
|
14
|
+
* Create a mock Docker container.
|
|
15
|
+
*/
|
|
16
|
+
export const createMockContainer = (config) => ({
|
|
17
|
+
id: config.id,
|
|
18
|
+
start: () => Promise.resolve(),
|
|
19
|
+
stop: () => Promise.resolve(),
|
|
20
|
+
kill: () => Promise.resolve(),
|
|
21
|
+
remove: () => Promise.resolve(),
|
|
22
|
+
inspect: () => Promise.resolve({
|
|
23
|
+
Id: config.id,
|
|
24
|
+
Name: `/${config.name}`,
|
|
25
|
+
State: { Running: config.running ?? true },
|
|
26
|
+
NetworkSettings: { Networks: {} },
|
|
27
|
+
}),
|
|
28
|
+
logs: () => Promise.resolve(Buffer.from(config.logs ?? '')),
|
|
29
|
+
stats: () => Promise.resolve(config.stats ?? {
|
|
30
|
+
cpu_stats: {
|
|
31
|
+
cpu_usage: { total_usage: 1000000 },
|
|
32
|
+
system_cpu_usage: 100000000,
|
|
33
|
+
online_cpus: 4,
|
|
34
|
+
},
|
|
35
|
+
precpu_stats: {
|
|
36
|
+
cpu_usage: { total_usage: 900000 },
|
|
37
|
+
system_cpu_usage: 99000000,
|
|
38
|
+
},
|
|
39
|
+
memory_stats: {
|
|
40
|
+
usage: 1024 * 1024 * 100,
|
|
41
|
+
limit: 1024 * 1024 * 1024,
|
|
42
|
+
},
|
|
43
|
+
networks: {},
|
|
44
|
+
}),
|
|
45
|
+
exec: () => Promise.resolve({
|
|
46
|
+
start: () => Promise.resolve({}),
|
|
47
|
+
inspect: () => Promise.resolve({ ExitCode: 0 }),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Create a mock Docker network.
|
|
52
|
+
*/
|
|
53
|
+
export const createMockNetwork = (id, name = 'mock-network') => ({
|
|
54
|
+
id,
|
|
55
|
+
inspect: () => Promise.resolve({ Id: id, Name: name }),
|
|
56
|
+
remove: () => Promise.resolve(),
|
|
57
|
+
connect: () => Promise.resolve(),
|
|
58
|
+
disconnect: () => Promise.resolve(),
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Create a mock DockerClient layer with customizable behavior.
|
|
62
|
+
*/
|
|
63
|
+
export const createMockDockerClient = (overrides = {}) => Layer.succeed(DockerClient, {
|
|
64
|
+
_tag: 'DockerClient',
|
|
65
|
+
client: {},
|
|
66
|
+
ping: () => Effect.void,
|
|
67
|
+
version: () => Effect.succeed({
|
|
68
|
+
Version: '24.0.0',
|
|
69
|
+
ApiVersion: '1.43',
|
|
70
|
+
Os: 'linux',
|
|
71
|
+
Arch: 'amd64',
|
|
72
|
+
}),
|
|
73
|
+
listContainers: () => Effect.succeed([]),
|
|
74
|
+
getContainer: (id) => createMockContainer({ id, name: id }),
|
|
75
|
+
createContainer: (options) => Effect.succeed(createMockContainer({ id: `mock-${options.name}`, name: options.name })),
|
|
76
|
+
listNetworks: () => Effect.succeed([]),
|
|
77
|
+
getNetwork: (id) => createMockNetwork(id),
|
|
78
|
+
createNetwork: (options) => Effect.succeed(createMockNetwork(`mock-${options.Name}`, options.Name)),
|
|
79
|
+
getImage: (name) => ({
|
|
80
|
+
inspect: () => Promise.resolve({ Id: name }),
|
|
81
|
+
remove: () => Promise.resolve(),
|
|
82
|
+
}),
|
|
83
|
+
listImages: () => Effect.succeed([]),
|
|
84
|
+
pullImage: () => Effect.void,
|
|
85
|
+
...overrides,
|
|
86
|
+
});
|
|
87
|
+
// ============ Mock RateLimiter ============
|
|
88
|
+
/**
|
|
89
|
+
* Create a mock RateLimiter that doesn't limit.
|
|
90
|
+
*/
|
|
91
|
+
export const MockDockerRateLimiter = Layer.succeed(DockerRateLimiter, {
|
|
92
|
+
_tag: 'DockerRateLimiter',
|
|
93
|
+
withPermit: (effect) => effect,
|
|
94
|
+
withPermits: () => (effect) => effect,
|
|
95
|
+
available: () => Effect.succeed(Infinity),
|
|
96
|
+
});
|
|
97
|
+
// ============ Mock CircuitBreaker ============
|
|
98
|
+
/**
|
|
99
|
+
* Create a mock CircuitBreaker that never opens.
|
|
100
|
+
*/
|
|
101
|
+
export const MockCircuitBreaker = Layer.succeed(CircuitBreaker, {
|
|
102
|
+
_tag: 'CircuitBreaker',
|
|
103
|
+
protect: (effect) => effect,
|
|
104
|
+
getState: () => Effect.succeed('Closed'),
|
|
105
|
+
reset: () => Effect.void,
|
|
106
|
+
getFailures: () => Effect.succeed(0),
|
|
107
|
+
});
|
|
108
|
+
// ============ Mock ResourceTracker ============
|
|
109
|
+
/**
|
|
110
|
+
* Create a mock ResourceTracker that tracks in memory.
|
|
111
|
+
*/
|
|
112
|
+
export const createMockResourceTracker = () => Effect.gen(function* () {
|
|
113
|
+
const resources = yield* Ref.make(new Map());
|
|
114
|
+
return {
|
|
115
|
+
_tag: 'ResourceTracker',
|
|
116
|
+
register: (resource) => Ref.update(resources, (map) => {
|
|
117
|
+
const newMap = new Map(map);
|
|
118
|
+
newMap.set(resource.id, resource);
|
|
119
|
+
return newMap;
|
|
120
|
+
}),
|
|
121
|
+
unregister: (id) => Ref.update(resources, (map) => {
|
|
122
|
+
const newMap = new Map(map);
|
|
123
|
+
newMap.delete(id);
|
|
124
|
+
return newMap;
|
|
125
|
+
}),
|
|
126
|
+
get: (id) => Ref.get(resources).pipe(Effect.map((map) => Option.fromNullable(map.get(id)))),
|
|
127
|
+
getAll: () => Ref.get(resources).pipe(Effect.map((map) => [...map.values()])),
|
|
128
|
+
getByType: (type) => Ref.get(resources).pipe(Effect.map((map) => [...map.values()].filter((r) => r.type === type))),
|
|
129
|
+
count: () => Ref.get(resources).pipe(Effect.map((map) => map.size)),
|
|
130
|
+
destroyAll: () => Ref.set(resources, new Map()).pipe(Effect.asVoid),
|
|
131
|
+
destroyByType: (_type) => Effect.void,
|
|
132
|
+
clear: () => Ref.set(resources, new Map()),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
export const MockResourceTracker = Layer.effect(ResourceTracker, createMockResourceTracker());
|
|
136
|
+
// ============ Mock EventBus ============
|
|
137
|
+
/**
|
|
138
|
+
* Create a mock EventBus that collects events.
|
|
139
|
+
*/
|
|
140
|
+
export const createMockEventBus = () => Effect.gen(function* () {
|
|
141
|
+
const events = yield* Ref.make([]);
|
|
142
|
+
const pubsub = yield* PubSub.unbounded();
|
|
143
|
+
const service = {
|
|
144
|
+
_tag: 'EventBus',
|
|
145
|
+
publish: (event) => Effect.all([
|
|
146
|
+
Ref.update(events, (arr) => [...arr, event]),
|
|
147
|
+
PubSub.publish(pubsub, event),
|
|
148
|
+
]).pipe(Effect.map(() => true)),
|
|
149
|
+
subscribe: () => Effect.gen(function* () {
|
|
150
|
+
const queue = yield* PubSub.subscribe(pubsub);
|
|
151
|
+
return Stream.fromQueue(queue);
|
|
152
|
+
}),
|
|
153
|
+
subscribeContainers: () => Effect.gen(function* () {
|
|
154
|
+
const queue = yield* PubSub.subscribe(pubsub);
|
|
155
|
+
return Stream.fromQueue(queue).pipe(Stream.filter((e) => e._tag === 'ContainerEvent'));
|
|
156
|
+
}),
|
|
157
|
+
subscribeNetworks: () => Effect.gen(function* () {
|
|
158
|
+
const queue = yield* PubSub.subscribe(pubsub);
|
|
159
|
+
return Stream.fromQueue(queue).pipe(Stream.filter((e) => e._tag === 'NetworkEvent'));
|
|
160
|
+
}),
|
|
161
|
+
subscribeDatabases: () => Effect.gen(function* () {
|
|
162
|
+
const queue = yield* PubSub.subscribe(pubsub);
|
|
163
|
+
return Stream.fromQueue(queue).pipe(Stream.filter((e) => e._tag === 'DatabaseEvent'));
|
|
164
|
+
}),
|
|
165
|
+
subscriberCount: () => Effect.succeed(0),
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
service,
|
|
169
|
+
getEvents: () => Ref.get(events),
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
// ============ Mock ReadinessCoordinator ============
|
|
173
|
+
/**
|
|
174
|
+
* Create a mock ReadinessCoordinator.
|
|
175
|
+
*/
|
|
176
|
+
export const createMockReadinessCoordinator = (preReady = []) => Effect.gen(function* () {
|
|
177
|
+
const state = yield* Ref.make(new Map(preReady.map((id) => [id, true])));
|
|
178
|
+
return {
|
|
179
|
+
_tag: 'ReadinessCoordinator',
|
|
180
|
+
waitForReady: (containerId, _healthCheck, _checkFn, _options) => Ref.update(state, (map) => {
|
|
181
|
+
const newMap = new Map(map);
|
|
182
|
+
newMap.set(containerId, true);
|
|
183
|
+
return newMap;
|
|
184
|
+
}).pipe(Effect.asVoid),
|
|
185
|
+
isReady: (containerId) => Ref.get(state).pipe(Effect.map((map) => map.get(containerId) ?? false)),
|
|
186
|
+
markReady: (containerId) => Ref.update(state, (map) => {
|
|
187
|
+
const newMap = new Map(map);
|
|
188
|
+
newMap.set(containerId, true);
|
|
189
|
+
return newMap;
|
|
190
|
+
}),
|
|
191
|
+
markNotReady: (containerId) => Ref.update(state, (map) => {
|
|
192
|
+
const newMap = new Map(map);
|
|
193
|
+
newMap.set(containerId, false);
|
|
194
|
+
return newMap;
|
|
195
|
+
}),
|
|
196
|
+
untrack: (containerId) => Ref.update(state, (map) => {
|
|
197
|
+
const newMap = new Map(map);
|
|
198
|
+
newMap.delete(containerId);
|
|
199
|
+
return newMap;
|
|
200
|
+
}),
|
|
201
|
+
getAll: () => Ref.get(state).pipe(Effect.map((map) => [...map.entries()].map(([containerId, ready]) => ({
|
|
202
|
+
containerId,
|
|
203
|
+
ready,
|
|
204
|
+
timestamp: new Date(),
|
|
205
|
+
})))),
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
// ============ Mock HarpoonConfig ============
|
|
209
|
+
/**
|
|
210
|
+
* Create a test configuration with fast timeouts.
|
|
211
|
+
*/
|
|
212
|
+
export const createMockHarpoonConfig = (overrides = {}) => Layer.succeed(HarpoonConfig, {
|
|
213
|
+
_tag: 'HarpoonConfig',
|
|
214
|
+
dockerSocket: undefined,
|
|
215
|
+
defaultTimeout: Duration.millis(5000),
|
|
216
|
+
cleanupTimeout: Duration.millis(1000),
|
|
217
|
+
logLevel: 'debug',
|
|
218
|
+
parallelLimit: 2,
|
|
219
|
+
retryAttempts: 1,
|
|
220
|
+
retryDelay: Duration.millis(100),
|
|
221
|
+
dockerStartupTimeout: Duration.millis(5000),
|
|
222
|
+
...overrides,
|
|
223
|
+
});
|
|
224
|
+
export const MockHarpoonConfig = createMockHarpoonConfig();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Runtime Process Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides process execution utilities that work with Node.js, Bun, and Deno.
|
|
5
|
+
* Uses Node.js child_process module which is compatible across all major runtimes.
|
|
6
|
+
*/
|
|
7
|
+
export interface SpawnResult {
|
|
8
|
+
exitCode: number;
|
|
9
|
+
stdout: string;
|
|
10
|
+
stderr: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Convert a readable stream to a string.
|
|
14
|
+
*/
|
|
15
|
+
export declare function streamToString(stream: NodeJS.ReadableStream): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Execute a command and capture output.
|
|
18
|
+
* Works with Node.js, Bun, and Deno (via Node compat).
|
|
19
|
+
*/
|
|
20
|
+
export declare function execCommand(command: string, args: string[], options?: {
|
|
21
|
+
captureStdout?: boolean;
|
|
22
|
+
captureStderr?: boolean;
|
|
23
|
+
}): Promise<SpawnResult>;
|
|
24
|
+
//# sourceMappingURL=process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE;IACP,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACpB,GACL,OAAO,CAAC,WAAW,CAAC,CA6BtB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Runtime Process Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides process execution utilities that work with Node.js, Bun, and Deno.
|
|
5
|
+
* Uses Node.js child_process module which is compatible across all major runtimes.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Convert a readable stream to a string.
|
|
10
|
+
*/
|
|
11
|
+
export function streamToString(stream) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
15
|
+
stream.on('error', reject);
|
|
16
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute a command and capture output.
|
|
21
|
+
* Works with Node.js, Bun, and Deno (via Node compat).
|
|
22
|
+
*/
|
|
23
|
+
export function execCommand(command, args, options = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const proc = spawn(command, args, {
|
|
26
|
+
stdio: [
|
|
27
|
+
'ignore',
|
|
28
|
+
options.captureStdout ? 'pipe' : 'ignore',
|
|
29
|
+
options.captureStderr ? 'pipe' : 'ignore',
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
if (proc.stdout) {
|
|
35
|
+
proc.stdout.on('data', (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (proc.stderr) {
|
|
40
|
+
proc.stderr.on('data', (data) => {
|
|
41
|
+
stderr += data.toString();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
proc.on('error', reject);
|
|
45
|
+
proc.on('close', (exitCode) => {
|
|
46
|
+
resolve({ exitCode: exitCode ?? 1, stdout, stderr });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docker-harpoon/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.2.0",
|
|
5
4
|
"description": "Core Docker resource primitives and binding interface",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
6
9
|
"type": "module",
|
|
7
10
|
"main": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -10,11 +13,12 @@
|
|
|
10
13
|
".": {
|
|
11
14
|
"types": "./dist/index.d.ts",
|
|
12
15
|
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./testing": {
|
|
18
|
+
"types": "./dist/testing/index.d.ts",
|
|
19
|
+
"import": "./dist/testing/index.js"
|
|
13
20
|
}
|
|
14
21
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist"
|
|
17
|
-
],
|
|
18
22
|
"scripts": {
|
|
19
23
|
"build": "tsgo",
|
|
20
24
|
"typecheck": "tsgo --noEmit",
|
|
@@ -23,9 +27,9 @@
|
|
|
23
27
|
"dependencies": {
|
|
24
28
|
"dockerode": "^4.0.9",
|
|
25
29
|
"effect": "^3.19.14",
|
|
26
|
-
"zod": "^
|
|
30
|
+
"zod": "^3.23.8"
|
|
27
31
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@effect/vitest": "^0.10.0"
|
|
30
34
|
}
|
|
31
35
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bindings.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bindings Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Dogfoods the binding system to verify:
|
|
5
|
-
* - createEnvBinding helper
|
|
6
|
-
* - Env var injection into containers
|
|
7
|
-
* - Binding composition
|
|
8
|
-
*/
|
|
9
|
-
import { describe, test, expect, afterEach, beforeAll, setDefaultTimeout } from 'bun:test';
|
|
10
|
-
import { Container, database, createEnvBinding, destroyAll } from '../index';
|
|
11
|
-
import { buildTestImage, TEST_IMAGE } from './test-setup';
|
|
12
|
-
setDefaultTimeout(30_000);
|
|
13
|
-
const waitUntil = (fn, timeout = 5000) => {
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const interval = setInterval(() => {
|
|
16
|
-
if (fn()) {
|
|
17
|
-
clearInterval(interval);
|
|
18
|
-
resolve(true);
|
|
19
|
-
}
|
|
20
|
-
}, 100);
|
|
21
|
-
});
|
|
22
|
-
};
|
|
23
|
-
describe('Bindings', () => {
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
await buildTestImage();
|
|
26
|
-
});
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
await destroyAll();
|
|
29
|
-
});
|
|
30
|
-
test('createEnvBinding creates a simple env binding', () => {
|
|
31
|
-
const binding = createEnvBinding('config', {
|
|
32
|
-
NODE_ENV: 'test',
|
|
33
|
-
LOG_LEVEL: 'debug',
|
|
34
|
-
});
|
|
35
|
-
expect(binding.type).toBe('config');
|
|
36
|
-
expect(binding.getEnv()).toEqual({
|
|
37
|
-
NODE_ENV: 'test',
|
|
38
|
-
LOG_LEVEL: 'debug',
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
test('container receives env vars from bindings', async () => {
|
|
42
|
-
const configBinding = createEnvBinding('config', {
|
|
43
|
-
APP_NAME: 'test-app',
|
|
44
|
-
APP_VERSION: '1.0.0',
|
|
45
|
-
});
|
|
46
|
-
const container = await Container('test-binding-env', {
|
|
47
|
-
image: TEST_IMAGE,
|
|
48
|
-
cmd: ['sleep', '30'],
|
|
49
|
-
bindings: {
|
|
50
|
-
config: configBinding,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
await waitUntil(() => !!container.getIp('test-binding-env'), 5000);
|
|
54
|
-
const result = await container.exec([
|
|
55
|
-
'sh',
|
|
56
|
-
'-c',
|
|
57
|
-
'echo "$APP_NAME:$APP_VERSION"',
|
|
58
|
-
]);
|
|
59
|
-
expect(result.stdout.trim()).toBe('test-app:1.0.0');
|
|
60
|
-
await container.destroy();
|
|
61
|
-
});
|
|
62
|
-
test('multiple bindings merge env vars', async () => {
|
|
63
|
-
const binding1 = createEnvBinding('env1', {
|
|
64
|
-
VAR_A: 'a',
|
|
65
|
-
VAR_B: 'b',
|
|
66
|
-
});
|
|
67
|
-
const binding2 = createEnvBinding('env2', {
|
|
68
|
-
VAR_C: 'c',
|
|
69
|
-
VAR_D: 'd',
|
|
70
|
-
});
|
|
71
|
-
const container = await Container('test-multi-binding', {
|
|
72
|
-
image: TEST_IMAGE,
|
|
73
|
-
cmd: ['sleep', '30'],
|
|
74
|
-
bindings: {
|
|
75
|
-
first: binding1,
|
|
76
|
-
second: binding2,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
await waitUntil(() => !!container.getIp('test-multi-binding'), 5000);
|
|
80
|
-
const result = await container.exec([
|
|
81
|
-
'sh',
|
|
82
|
-
'-c',
|
|
83
|
-
'echo "$VAR_A$VAR_B$VAR_C$VAR_D"',
|
|
84
|
-
]);
|
|
85
|
-
expect(result.stdout.trim()).toBe('abcd');
|
|
86
|
-
await container.destroy();
|
|
87
|
-
});
|
|
88
|
-
test('explicit env takes precedence over bindings', async () => {
|
|
89
|
-
const binding = createEnvBinding('config', {
|
|
90
|
-
MY_VAR: 'from-binding',
|
|
91
|
-
});
|
|
92
|
-
const container = await Container('test-env-precedence', {
|
|
93
|
-
image: TEST_IMAGE,
|
|
94
|
-
cmd: ['sleep', '30'],
|
|
95
|
-
bindings: {
|
|
96
|
-
config: binding,
|
|
97
|
-
},
|
|
98
|
-
env: {
|
|
99
|
-
MY_VAR: 'from-explicit',
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
await waitUntil(() => !!container.getIp('test-env-precedence'), 5000);
|
|
103
|
-
const result = await container.exec(['sh', '-c', 'echo "$MY_VAR"']);
|
|
104
|
-
expect(result.stdout.trim()).toBe('from-explicit');
|
|
105
|
-
await container.destroy();
|
|
106
|
-
});
|
|
107
|
-
test('database can be used as binding resource', async () => {
|
|
108
|
-
const db = await database('test-db-binding', {
|
|
109
|
-
image: 'redis:7-alpine',
|
|
110
|
-
hostPort: 16390,
|
|
111
|
-
});
|
|
112
|
-
const dbBinding = createEnvBinding('database', {
|
|
113
|
-
REDIS_URL: db.connectionString,
|
|
114
|
-
});
|
|
115
|
-
const container = await Container('test-db-consumer', {
|
|
116
|
-
image: TEST_IMAGE,
|
|
117
|
-
cmd: ['sleep', '30'],
|
|
118
|
-
bindings: {
|
|
119
|
-
db: dbBinding,
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
await waitUntil(() => !!container.getIp('test-db-consumer'), 5000);
|
|
123
|
-
const result = await container.exec(['sh', '-c', 'echo "$REDIS_URL"']);
|
|
124
|
-
expect(result.stdout.trim()).toBe('redis://localhost:16390');
|
|
125
|
-
await container.destroy();
|
|
126
|
-
await db.destroy();
|
|
127
|
-
});
|
|
128
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Container Resource Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Dogfoods the Container resource to verify:
|
|
5
|
-
* - Creation with network binding
|
|
6
|
-
* - waitForLog functionality
|
|
7
|
-
* - exec command execution
|
|
8
|
-
* - stats retrieval
|
|
9
|
-
* - Lifecycle (stop, destroy)
|
|
10
|
-
*/
|
|
11
|
-
export {};
|
|
12
|
-
//# sourceMappingURL=container.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"container.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/container.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Container Resource Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Dogfoods the Container resource to verify:
|
|
5
|
-
* - Creation with network binding
|
|
6
|
-
* - waitForLog functionality
|
|
7
|
-
* - exec command execution
|
|
8
|
-
* - stats retrieval
|
|
9
|
-
* - Lifecycle (stop, destroy)
|
|
10
|
-
*/
|
|
11
|
-
import { describe, test, expect, afterEach, beforeAll, setDefaultTimeout } from 'bun:test';
|
|
12
|
-
import { Network, Container, destroyAll } from '../index';
|
|
13
|
-
import { buildTestImage, TEST_IMAGE } from './test-setup';
|
|
14
|
-
setDefaultTimeout(30_000);
|
|
15
|
-
describe('Container', () => {
|
|
16
|
-
beforeAll(async () => {
|
|
17
|
-
await buildTestImage();
|
|
18
|
-
});
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await destroyAll();
|
|
21
|
-
});
|
|
22
|
-
test('creates a container with basic config', async () => {
|
|
23
|
-
const container = await Container('test-container-basic', {
|
|
24
|
-
image: TEST_IMAGE,
|
|
25
|
-
cmd: ['sleep', '30'],
|
|
26
|
-
});
|
|
27
|
-
expect(container.id).toBeDefined();
|
|
28
|
-
expect(container.name).toBe('test-container-basic');
|
|
29
|
-
await container.destroy();
|
|
30
|
-
});
|
|
31
|
-
test('creates a container attached to a network', async () => {
|
|
32
|
-
const network = await Network('test-container-net');
|
|
33
|
-
const container = await Container('test-container-networked', {
|
|
34
|
-
image: TEST_IMAGE,
|
|
35
|
-
cmd: ['sleep', '30'],
|
|
36
|
-
networks: [network],
|
|
37
|
-
});
|
|
38
|
-
expect(container.id).toBeDefined();
|
|
39
|
-
const ip = await container.getIp('test-container-net');
|
|
40
|
-
expect(ip).toBeDefined();
|
|
41
|
-
await container.destroy();
|
|
42
|
-
await network.destroy();
|
|
43
|
-
});
|
|
44
|
-
test('exec runs commands inside container', async () => {
|
|
45
|
-
const container = await Container('test-container-exec', {
|
|
46
|
-
image: TEST_IMAGE,
|
|
47
|
-
cmd: ['sleep', '30'],
|
|
48
|
-
});
|
|
49
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
50
|
-
const result = await container.exec(['echo', 'hello world']);
|
|
51
|
-
expect(result.exitCode).toBe(0);
|
|
52
|
-
expect(result.stdout.trim()).toBe('hello world');
|
|
53
|
-
expect(result.durationMs).toBeGreaterThan(0);
|
|
54
|
-
await container.destroy();
|
|
55
|
-
});
|
|
56
|
-
test('stats returns container resource statistics', async () => {
|
|
57
|
-
const container = await Container('test-container-stats', {
|
|
58
|
-
image: TEST_IMAGE,
|
|
59
|
-
cmd: ['sleep', '30'],
|
|
60
|
-
});
|
|
61
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
62
|
-
const stats = await container.stats();
|
|
63
|
-
expect(stats.cpu).toBeDefined();
|
|
64
|
-
expect(stats.cpu.cores).toBeGreaterThan(0);
|
|
65
|
-
expect(stats.memory).toBeDefined();
|
|
66
|
-
expect(stats.memory.limit).toBeGreaterThan(0);
|
|
67
|
-
expect(stats.network).toBeDefined();
|
|
68
|
-
expect(stats.containerId).toBe(container.id);
|
|
69
|
-
await container.destroy();
|
|
70
|
-
});
|
|
71
|
-
test('waitForLog detects log patterns', async () => {
|
|
72
|
-
const container = await Container('test-container-logs', {
|
|
73
|
-
image: TEST_IMAGE,
|
|
74
|
-
cmd: ['sh', '-c', 'echo "Server started successfully" && sleep 30'],
|
|
75
|
-
});
|
|
76
|
-
await container.waitForLog('Server started', 5000);
|
|
77
|
-
expect(true).toBe(true);
|
|
78
|
-
await container.destroy();
|
|
79
|
-
});
|
|
80
|
-
test('stop gracefully shuts down container', async () => {
|
|
81
|
-
const container = await Container('test-container-stop', {
|
|
82
|
-
image: TEST_IMAGE,
|
|
83
|
-
cmd: ['sh', '-c', 'trap "exit 0" TERM; while true; do sleep 1; done'],
|
|
84
|
-
});
|
|
85
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
86
|
-
const metadata = await container.stop('SIGTERM', 10000);
|
|
87
|
-
expect(metadata.signal).toBe('SIGTERM');
|
|
88
|
-
expect(metadata.signalCount).toBeGreaterThan(0);
|
|
89
|
-
expect(metadata.timeTakenMs).toBeGreaterThan(0);
|
|
90
|
-
await container.destroy();
|
|
91
|
-
});
|
|
92
|
-
test('supports environment variables', async () => {
|
|
93
|
-
const container = await Container('test-container-env', {
|
|
94
|
-
image: TEST_IMAGE,
|
|
95
|
-
cmd: ['sleep', '30'],
|
|
96
|
-
env: {
|
|
97
|
-
MY_VAR: 'hello',
|
|
98
|
-
ANOTHER_VAR: 'world',
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
102
|
-
const result = await container.exec([
|
|
103
|
-
'sh',
|
|
104
|
-
'-c',
|
|
105
|
-
'echo $MY_VAR $ANOTHER_VAR',
|
|
106
|
-
]);
|
|
107
|
-
expect(result.stdout.trim()).toBe('hello world');
|
|
108
|
-
await container.destroy();
|
|
109
|
-
});
|
|
110
|
-
test('supports port mappings', async () => {
|
|
111
|
-
const container = await Container('test-container-ports', {
|
|
112
|
-
image: TEST_IMAGE,
|
|
113
|
-
cmd: ['sleep', '30'],
|
|
114
|
-
ports: [{ internal: 8080, external: 18080 }],
|
|
115
|
-
});
|
|
116
|
-
expect(container.id).toBeDefined();
|
|
117
|
-
await container.destroy();
|
|
118
|
-
});
|
|
119
|
-
test('streamLogs terminates without follow option', async () => {
|
|
120
|
-
const container = await Container('test-logs-terminate', {
|
|
121
|
-
image: TEST_IMAGE,
|
|
122
|
-
cmd: ['sh', '-c', 'echo "line1" && echo "line2" && echo "line3" && sleep 30'],
|
|
123
|
-
});
|
|
124
|
-
await container.waitForLog('line3', 5000);
|
|
125
|
-
const lines = [];
|
|
126
|
-
const logStream = await container.streamLogs({ tail: 3 });
|
|
127
|
-
for await (const line of logStream) {
|
|
128
|
-
lines.push(line.message);
|
|
129
|
-
}
|
|
130
|
-
// Loop should exit naturally
|
|
131
|
-
expect(lines).toContain('line1');
|
|
132
|
-
expect(lines).toContain('line2');
|
|
133
|
-
expect(lines).toContain('line3');
|
|
134
|
-
await container.destroy();
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"database.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/database.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|