@docker-harpoon/core 0.1.3 → 0.1.5
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/__tests__/bindings.test.js +9 -5
- package/dist/__tests__/container.test.js +30 -9
- package/dist/__tests__/database.test.js +0 -14
- package/dist/__tests__/docker-infra.template.d.ts +2 -0
- package/dist/__tests__/docker-infra.template.d.ts.map +1 -0
- package/dist/__tests__/docker-infra.template.js +174 -0
- package/dist/__tests__/test-setup.d.ts +9 -0
- package/dist/__tests__/test-setup.d.ts.map +1 -0
- package/dist/__tests__/test-setup.js +27 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/promise.d.ts +13 -3
- package/dist/api/promise.d.ts.map +1 -1
- package/dist/api/promise.js +33 -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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/resources/container.d.ts +25 -2
- package/dist/resources/container.d.ts.map +1 -1
- package/dist/resources/container.js +301 -77
- package/dist/resources/image.d.ts.map +1 -1
- package/dist/resources/image.js +14 -35
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/network.d.ts.map +1 -1
- package/dist/resources/network.js +23 -9
- package/dist/resources/schemas.d.ts +178 -18
- package/dist/resources/schemas.d.ts.map +1 -1
- package/dist/resources/schemas.js +2 -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
|
@@ -8,24 +8,35 @@ import { Effect, Exit } from 'effect';
|
|
|
8
8
|
import { toHarpoonError, NetworkError } from '../errors';
|
|
9
9
|
// Constants
|
|
10
10
|
const CLEANUP_TIMEOUT_MS = 5_000; // 5 seconds max for network cleanup
|
|
11
|
+
const makeNetworkLogger = (networkName) => ({
|
|
12
|
+
debug: (message) => {
|
|
13
|
+
if (process.env.HARPOON_LOG_LEVEL === 'debug') {
|
|
14
|
+
console.log(`[Network:${networkName}] ${message}`);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
info: (message) => console.log(`[Network:${networkName}] ${message}`),
|
|
18
|
+
warn: (message) => console.warn(`[Network:${networkName}] ${message}`),
|
|
19
|
+
error: (message) => console.error(`[Network:${networkName}] ${message}`),
|
|
20
|
+
});
|
|
11
21
|
/**
|
|
12
22
|
* Safely removes a network, handling all possible states.
|
|
13
23
|
*/
|
|
14
24
|
const safeRemoveNetwork = async (docker, networkId, networkName) => {
|
|
25
|
+
const log = makeNetworkLogger(networkName);
|
|
15
26
|
try {
|
|
16
27
|
const network = docker.getNetwork(networkId);
|
|
17
28
|
await network.remove();
|
|
18
|
-
|
|
29
|
+
log.info('Network removed');
|
|
19
30
|
}
|
|
20
31
|
catch (e) {
|
|
21
32
|
if (e.statusCode === 404) {
|
|
22
|
-
|
|
33
|
+
log.debug('Network already removed');
|
|
23
34
|
}
|
|
24
35
|
else if (e.message?.includes('has active endpoints')) {
|
|
25
|
-
|
|
36
|
+
log.warn('Network has active endpoints, skipping removal');
|
|
26
37
|
}
|
|
27
38
|
else {
|
|
28
|
-
|
|
39
|
+
log.error(`Error removing network: ${e.message}`);
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
};
|
|
@@ -41,21 +52,22 @@ Effect.tryPromise({
|
|
|
41
52
|
filters: { name: [config.name] },
|
|
42
53
|
});
|
|
43
54
|
const existing = networks.find((n) => n.Name === config.name);
|
|
55
|
+
const log = makeNetworkLogger(config.name);
|
|
44
56
|
if (existing) {
|
|
45
57
|
// Try to remove existing network for clean state
|
|
46
58
|
try {
|
|
47
59
|
const net = docker.getNetwork(existing.Id);
|
|
48
60
|
await net.remove();
|
|
49
|
-
|
|
61
|
+
log.info('Removed existing network');
|
|
50
62
|
}
|
|
51
63
|
catch (e) {
|
|
52
64
|
if (e.message?.includes('has active endpoints')) {
|
|
53
65
|
// Network in use - try to reuse it
|
|
54
|
-
|
|
66
|
+
log.warn('Network in use, reusing existing network');
|
|
55
67
|
return { id: existing.Id, name: config.name };
|
|
56
68
|
}
|
|
57
69
|
// Other errors - try to proceed with creation anyway
|
|
58
|
-
|
|
70
|
+
log.warn(`Could not remove: ${e.message}`);
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
// Create new network
|
|
@@ -71,7 +83,7 @@ Effect.tryPromise({
|
|
|
71
83
|
(resource, exit) => Effect.gen(function* () {
|
|
72
84
|
const isInterrupted = Exit.isInterrupted(exit);
|
|
73
85
|
if (isInterrupted) {
|
|
74
|
-
|
|
86
|
+
yield* Effect.logDebug('Interrupted - cleaning up network').pipe(Effect.annotateLogs({ service: 'Network', networkId: resource.id }));
|
|
75
87
|
}
|
|
76
88
|
yield* Effect.tryPromise({
|
|
77
89
|
try: () => Promise.race([
|
|
@@ -79,7 +91,9 @@ Effect.tryPromise({
|
|
|
79
91
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Network cleanup timeout')), CLEANUP_TIMEOUT_MS)),
|
|
80
92
|
]),
|
|
81
93
|
catch: (e) => {
|
|
82
|
-
|
|
94
|
+
// Log error but don't fail - cleanup should be best effort
|
|
95
|
+
const log = makeNetworkLogger(resource.name);
|
|
96
|
+
log.error(`Failed to clean up network: ${e}`);
|
|
83
97
|
return null;
|
|
84
98
|
},
|
|
85
99
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Zod Schemas for Container Resources
|
|
3
3
|
*
|
|
4
4
|
* Defines validation schemas for container stats, exec, and log streaming.
|
|
5
|
-
* Following
|
|
5
|
+
* Following Harpoon patterns for declarative resource definitions.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
/**
|
|
@@ -13,7 +13,17 @@ export declare const cpuStatsSchema: z.ZodObject<{
|
|
|
13
13
|
system: z.ZodNumber;
|
|
14
14
|
percent: z.ZodNumber;
|
|
15
15
|
cores: z.ZodNumber;
|
|
16
|
-
}, z.
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
usage: number;
|
|
18
|
+
system: number;
|
|
19
|
+
percent: number;
|
|
20
|
+
cores: number;
|
|
21
|
+
}, {
|
|
22
|
+
usage: number;
|
|
23
|
+
system: number;
|
|
24
|
+
percent: number;
|
|
25
|
+
cores: number;
|
|
26
|
+
}>;
|
|
17
27
|
/**
|
|
18
28
|
* Memory statistics from Docker container
|
|
19
29
|
*/
|
|
@@ -23,7 +33,19 @@ export declare const memoryStatsSchema: z.ZodObject<{
|
|
|
23
33
|
percent: z.ZodNumber;
|
|
24
34
|
usageMB: z.ZodNumber;
|
|
25
35
|
limitMB: z.ZodNumber;
|
|
26
|
-
}, z.
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
usage: number;
|
|
38
|
+
limit: number;
|
|
39
|
+
percent: number;
|
|
40
|
+
usageMB: number;
|
|
41
|
+
limitMB: number;
|
|
42
|
+
}, {
|
|
43
|
+
usage: number;
|
|
44
|
+
limit: number;
|
|
45
|
+
percent: number;
|
|
46
|
+
usageMB: number;
|
|
47
|
+
limitMB: number;
|
|
48
|
+
}>;
|
|
27
49
|
/**
|
|
28
50
|
* Network I/O statistics from Docker container
|
|
29
51
|
*/
|
|
@@ -32,7 +54,17 @@ export declare const networkStatsSchema: z.ZodObject<{
|
|
|
32
54
|
txBytes: z.ZodNumber;
|
|
33
55
|
rxPackets: z.ZodNumber;
|
|
34
56
|
txPackets: z.ZodNumber;
|
|
35
|
-
}, z.
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
rxBytes: number;
|
|
59
|
+
txBytes: number;
|
|
60
|
+
rxPackets: number;
|
|
61
|
+
txPackets: number;
|
|
62
|
+
}, {
|
|
63
|
+
rxBytes: number;
|
|
64
|
+
txBytes: number;
|
|
65
|
+
rxPackets: number;
|
|
66
|
+
txPackets: number;
|
|
67
|
+
}>;
|
|
36
68
|
/**
|
|
37
69
|
* Complete container statistics
|
|
38
70
|
*/
|
|
@@ -42,23 +74,99 @@ export declare const containerStatsSchema: z.ZodObject<{
|
|
|
42
74
|
system: z.ZodNumber;
|
|
43
75
|
percent: z.ZodNumber;
|
|
44
76
|
cores: z.ZodNumber;
|
|
45
|
-
}, z.
|
|
77
|
+
}, "strip", z.ZodTypeAny, {
|
|
78
|
+
usage: number;
|
|
79
|
+
system: number;
|
|
80
|
+
percent: number;
|
|
81
|
+
cores: number;
|
|
82
|
+
}, {
|
|
83
|
+
usage: number;
|
|
84
|
+
system: number;
|
|
85
|
+
percent: number;
|
|
86
|
+
cores: number;
|
|
87
|
+
}>;
|
|
46
88
|
memory: z.ZodObject<{
|
|
47
89
|
usage: z.ZodNumber;
|
|
48
90
|
limit: z.ZodNumber;
|
|
49
91
|
percent: z.ZodNumber;
|
|
50
92
|
usageMB: z.ZodNumber;
|
|
51
93
|
limitMB: z.ZodNumber;
|
|
52
|
-
}, z.
|
|
94
|
+
}, "strip", z.ZodTypeAny, {
|
|
95
|
+
usage: number;
|
|
96
|
+
limit: number;
|
|
97
|
+
percent: number;
|
|
98
|
+
usageMB: number;
|
|
99
|
+
limitMB: number;
|
|
100
|
+
}, {
|
|
101
|
+
usage: number;
|
|
102
|
+
limit: number;
|
|
103
|
+
percent: number;
|
|
104
|
+
usageMB: number;
|
|
105
|
+
limitMB: number;
|
|
106
|
+
}>;
|
|
53
107
|
network: z.ZodObject<{
|
|
54
108
|
rxBytes: z.ZodNumber;
|
|
55
109
|
txBytes: z.ZodNumber;
|
|
56
110
|
rxPackets: z.ZodNumber;
|
|
57
111
|
txPackets: z.ZodNumber;
|
|
58
|
-
}, z.
|
|
112
|
+
}, "strip", z.ZodTypeAny, {
|
|
113
|
+
rxBytes: number;
|
|
114
|
+
txBytes: number;
|
|
115
|
+
rxPackets: number;
|
|
116
|
+
txPackets: number;
|
|
117
|
+
}, {
|
|
118
|
+
rxBytes: number;
|
|
119
|
+
txBytes: number;
|
|
120
|
+
rxPackets: number;
|
|
121
|
+
txPackets: number;
|
|
122
|
+
}>;
|
|
59
123
|
timestamp: z.ZodString;
|
|
60
124
|
containerId: z.ZodString;
|
|
61
|
-
}, z.
|
|
125
|
+
}, "strip", z.ZodTypeAny, {
|
|
126
|
+
cpu: {
|
|
127
|
+
usage: number;
|
|
128
|
+
system: number;
|
|
129
|
+
percent: number;
|
|
130
|
+
cores: number;
|
|
131
|
+
};
|
|
132
|
+
memory: {
|
|
133
|
+
usage: number;
|
|
134
|
+
limit: number;
|
|
135
|
+
percent: number;
|
|
136
|
+
usageMB: number;
|
|
137
|
+
limitMB: number;
|
|
138
|
+
};
|
|
139
|
+
network: {
|
|
140
|
+
rxBytes: number;
|
|
141
|
+
txBytes: number;
|
|
142
|
+
rxPackets: number;
|
|
143
|
+
txPackets: number;
|
|
144
|
+
};
|
|
145
|
+
timestamp: string;
|
|
146
|
+
containerId: string;
|
|
147
|
+
}, {
|
|
148
|
+
cpu: {
|
|
149
|
+
usage: number;
|
|
150
|
+
system: number;
|
|
151
|
+
percent: number;
|
|
152
|
+
cores: number;
|
|
153
|
+
};
|
|
154
|
+
memory: {
|
|
155
|
+
usage: number;
|
|
156
|
+
limit: number;
|
|
157
|
+
percent: number;
|
|
158
|
+
usageMB: number;
|
|
159
|
+
limitMB: number;
|
|
160
|
+
};
|
|
161
|
+
network: {
|
|
162
|
+
rxBytes: number;
|
|
163
|
+
txBytes: number;
|
|
164
|
+
rxPackets: number;
|
|
165
|
+
txPackets: number;
|
|
166
|
+
};
|
|
167
|
+
timestamp: string;
|
|
168
|
+
containerId: string;
|
|
169
|
+
}>;
|
|
62
170
|
export type ContainerStats = z.infer<typeof containerStatsSchema>;
|
|
63
171
|
export type CpuStats = z.infer<typeof cpuStatsSchema>;
|
|
64
172
|
export type MemoryStats = z.infer<typeof memoryStatsSchema>;
|
|
@@ -67,13 +175,27 @@ export type NetworkStats = z.infer<typeof networkStatsSchema>;
|
|
|
67
175
|
* Options for executing a command in a container
|
|
68
176
|
*/
|
|
69
177
|
export declare const execOptionsSchema: z.ZodObject<{
|
|
70
|
-
cmd: z.ZodArray<z.ZodString>;
|
|
178
|
+
cmd: z.ZodArray<z.ZodString, "many">;
|
|
71
179
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
72
180
|
workingDir: z.ZodOptional<z.ZodString>;
|
|
73
181
|
user: z.ZodOptional<z.ZodString>;
|
|
74
182
|
privileged: z.ZodOptional<z.ZodBoolean>;
|
|
75
183
|
tty: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
-
}, z.
|
|
184
|
+
}, "strip", z.ZodTypeAny, {
|
|
185
|
+
cmd: string[];
|
|
186
|
+
env?: Record<string, string> | undefined;
|
|
187
|
+
workingDir?: string | undefined;
|
|
188
|
+
user?: string | undefined;
|
|
189
|
+
privileged?: boolean | undefined;
|
|
190
|
+
tty?: boolean | undefined;
|
|
191
|
+
}, {
|
|
192
|
+
cmd: string[];
|
|
193
|
+
env?: Record<string, string> | undefined;
|
|
194
|
+
workingDir?: string | undefined;
|
|
195
|
+
user?: string | undefined;
|
|
196
|
+
privileged?: boolean | undefined;
|
|
197
|
+
tty?: boolean | undefined;
|
|
198
|
+
}>;
|
|
77
199
|
/**
|
|
78
200
|
* Result of executing a command in a container
|
|
79
201
|
*/
|
|
@@ -82,7 +204,17 @@ export declare const execResultSchema: z.ZodObject<{
|
|
|
82
204
|
stdout: z.ZodString;
|
|
83
205
|
stderr: z.ZodString;
|
|
84
206
|
durationMs: z.ZodNumber;
|
|
85
|
-
}, z.
|
|
207
|
+
}, "strip", z.ZodTypeAny, {
|
|
208
|
+
exitCode: number;
|
|
209
|
+
stdout: string;
|
|
210
|
+
stderr: string;
|
|
211
|
+
durationMs: number;
|
|
212
|
+
}, {
|
|
213
|
+
exitCode: number;
|
|
214
|
+
stdout: string;
|
|
215
|
+
stderr: string;
|
|
216
|
+
durationMs: number;
|
|
217
|
+
}>;
|
|
86
218
|
export type ExecOptions = z.infer<typeof execOptionsSchema>;
|
|
87
219
|
export type ExecResult = z.infer<typeof execResultSchema>;
|
|
88
220
|
/**
|
|
@@ -95,18 +227,40 @@ export declare const logOptionsSchema: z.ZodObject<{
|
|
|
95
227
|
until: z.ZodOptional<z.ZodNumber>;
|
|
96
228
|
tail: z.ZodOptional<z.ZodNumber>;
|
|
97
229
|
timestamps: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
-
|
|
230
|
+
follow: z.ZodOptional<z.ZodBoolean>;
|
|
231
|
+
}, "strip", z.ZodTypeAny, {
|
|
232
|
+
stdout?: boolean | undefined;
|
|
233
|
+
stderr?: boolean | undefined;
|
|
234
|
+
since?: number | undefined;
|
|
235
|
+
until?: number | undefined;
|
|
236
|
+
tail?: number | undefined;
|
|
237
|
+
timestamps?: boolean | undefined;
|
|
238
|
+
follow?: boolean | undefined;
|
|
239
|
+
}, {
|
|
240
|
+
stdout?: boolean | undefined;
|
|
241
|
+
stderr?: boolean | undefined;
|
|
242
|
+
since?: number | undefined;
|
|
243
|
+
until?: number | undefined;
|
|
244
|
+
tail?: number | undefined;
|
|
245
|
+
timestamps?: boolean | undefined;
|
|
246
|
+
follow?: boolean | undefined;
|
|
247
|
+
}>;
|
|
99
248
|
/**
|
|
100
249
|
* A single log line from a container
|
|
101
250
|
*/
|
|
102
251
|
export declare const logLineSchema: z.ZodObject<{
|
|
103
|
-
stream: z.ZodEnum<
|
|
104
|
-
stderr: "stderr";
|
|
105
|
-
stdout: "stdout";
|
|
106
|
-
}>;
|
|
252
|
+
stream: z.ZodEnum<["stdout", "stderr"]>;
|
|
107
253
|
timestamp: z.ZodOptional<z.ZodString>;
|
|
108
254
|
message: z.ZodString;
|
|
109
|
-
}, z.
|
|
255
|
+
}, "strip", z.ZodTypeAny, {
|
|
256
|
+
stream: "stderr" | "stdout";
|
|
257
|
+
timestamp?: string | undefined;
|
|
258
|
+
message: string;
|
|
259
|
+
}, {
|
|
260
|
+
stream: "stderr" | "stdout";
|
|
261
|
+
timestamp?: string | undefined;
|
|
262
|
+
message: string;
|
|
263
|
+
}>;
|
|
110
264
|
export type LogOptions = z.infer<typeof logOptionsSchema>;
|
|
111
265
|
export type LogLine = z.infer<typeof logLineSchema>;
|
|
112
266
|
/**
|
|
@@ -115,6 +269,12 @@ export type LogLine = z.infer<typeof logLineSchema>;
|
|
|
115
269
|
export declare const statsOptionsSchema: z.ZodObject<{
|
|
116
270
|
stream: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
117
271
|
oneShot: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
118
|
-
}, z.
|
|
272
|
+
}, "strip", z.ZodTypeAny, {
|
|
273
|
+
stream: boolean;
|
|
274
|
+
oneShot: boolean;
|
|
275
|
+
}, {
|
|
276
|
+
stream?: boolean | undefined;
|
|
277
|
+
oneShot?: boolean | undefined;
|
|
278
|
+
}>;
|
|
119
279
|
export type StatsOptions = z.infer<typeof statsOptionsSchema>;
|
|
120
280
|
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/resources/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;GAEG;AACH,eAAO,MAAM,cAAc
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/resources/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAKzB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;EAM5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAK7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAM/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;EAO5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;EAK3B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;GAEG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;EAQ3B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;EAIxB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;EAG7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Zod Schemas for Container Resources
|
|
3
3
|
*
|
|
4
4
|
* Defines validation schemas for container stats, exec, and log streaming.
|
|
5
|
-
* Following
|
|
5
|
+
* Following Harpoon patterns for declarative resource definitions.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
// ============ Container Stats Schema ============
|
|
@@ -76,6 +76,7 @@ export const logOptionsSchema = z.object({
|
|
|
76
76
|
until: z.number().optional().describe('Unix timestamp to end at'),
|
|
77
77
|
tail: z.number().int().min(0).optional().describe('Number of lines to tail'),
|
|
78
78
|
timestamps: z.boolean().optional().describe('Include timestamps (default: false)'),
|
|
79
|
+
follow: z.boolean().optional().describe('Follow log output (default: false)'),
|
|
79
80
|
});
|
|
80
81
|
/**
|
|
81
82
|
* A single log line from a container
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CircuitBreaker Service
|
|
3
|
+
*
|
|
4
|
+
* Implements the circuit breaker pattern to prevent cascading failures
|
|
5
|
+
* when the Docker daemon becomes unresponsive or overloaded.
|
|
6
|
+
*
|
|
7
|
+
* States:
|
|
8
|
+
* - Closed: Normal operation, requests pass through
|
|
9
|
+
* - Open: Circuit tripped, requests fail fast
|
|
10
|
+
* - HalfOpen: Testing if the service has recovered
|
|
11
|
+
*/
|
|
12
|
+
import { Context, Effect, Layer, Duration } from 'effect';
|
|
13
|
+
/**
|
|
14
|
+
* Circuit breaker states.
|
|
15
|
+
*/
|
|
16
|
+
export type CircuitState = 'Closed' | 'Open' | 'HalfOpen';
|
|
17
|
+
/**
|
|
18
|
+
* Circuit breaker configuration.
|
|
19
|
+
*/
|
|
20
|
+
export interface CircuitBreakerConfig {
|
|
21
|
+
/** Number of failures before opening the circuit */
|
|
22
|
+
readonly maxFailures: number;
|
|
23
|
+
/** Time to wait before attempting to close the circuit */
|
|
24
|
+
readonly resetTimeout: Duration.Duration;
|
|
25
|
+
/** Optional name for logging */
|
|
26
|
+
readonly name?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Error thrown when the circuit is open.
|
|
30
|
+
*/
|
|
31
|
+
export declare class CircuitOpenError extends Error {
|
|
32
|
+
readonly _tag = "CircuitOpenError";
|
|
33
|
+
readonly circuitName: string;
|
|
34
|
+
readonly resetTimeout: Duration.Duration;
|
|
35
|
+
constructor(circuitName: string, resetTimeout: Duration.Duration);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* CircuitBreaker service interface.
|
|
39
|
+
*/
|
|
40
|
+
export interface CircuitBreakerService {
|
|
41
|
+
readonly _tag: 'CircuitBreaker';
|
|
42
|
+
/**
|
|
43
|
+
* Execute an effect protected by the circuit breaker.
|
|
44
|
+
* If the circuit is open, fails fast with CircuitOpenError.
|
|
45
|
+
*/
|
|
46
|
+
readonly protect: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | CircuitOpenError, R>;
|
|
47
|
+
/**
|
|
48
|
+
* Get the current circuit state.
|
|
49
|
+
*/
|
|
50
|
+
readonly getState: () => Effect.Effect<CircuitState>;
|
|
51
|
+
/**
|
|
52
|
+
* Manually reset the circuit to closed state.
|
|
53
|
+
*/
|
|
54
|
+
readonly reset: () => Effect.Effect<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Get current failure count.
|
|
57
|
+
*/
|
|
58
|
+
readonly getFailures: () => Effect.Effect<number>;
|
|
59
|
+
}
|
|
60
|
+
declare const CircuitBreaker_base: Context.TagClass<CircuitBreaker, "@harpoon/CircuitBreaker", CircuitBreakerService>;
|
|
61
|
+
/**
|
|
62
|
+
* CircuitBreaker service tag for dependency injection.
|
|
63
|
+
*/
|
|
64
|
+
export declare class CircuitBreaker extends CircuitBreaker_base {
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Factory function to create a circuit breaker with specific configuration.
|
|
68
|
+
*/
|
|
69
|
+
export declare const makeCircuitBreaker: (config: CircuitBreakerConfig) => Effect.Effect<CircuitBreakerService, never, never>;
|
|
70
|
+
/**
|
|
71
|
+
* Live implementation with default configuration.
|
|
72
|
+
*/
|
|
73
|
+
export declare const CircuitBreakerLive: Layer.Layer<CircuitBreaker, never, never>;
|
|
74
|
+
/**
|
|
75
|
+
* Test implementation that never opens (all requests pass through).
|
|
76
|
+
*/
|
|
77
|
+
export declare const CircuitBreakerTest: Layer.Layer<CircuitBreaker, never, never>;
|
|
78
|
+
/**
|
|
79
|
+
* Create a custom circuit breaker layer with specific configuration.
|
|
80
|
+
*/
|
|
81
|
+
export declare const makeCircuitBreakerLayer: (config: CircuitBreakerConfig) => Layer.Layer<CircuitBreaker, never, never>;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=CircuitBreaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CircuitBreaker.d.ts","sourceRoot":"","sources":["../../src/services/CircuitBreaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAO,QAAQ,EAAS,MAAM,QAAQ,CAAC;AAEtE;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAEzC,gCAAgC;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAEzC,YAAY,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,QAAQ,EAQ/D;CACF;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAE/C;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAErD;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAE1C;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;CACnD;;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,mBAGjC;CAAG;AAEN;;GAEG;AACH,eAAO,MAAM,kBAAkB,sFA+I3B,CAAC;AAWL;;GAEG;AACH,eAAO,MAAM,kBAAkB,2CAAkE,CAAC;AAElG;;GAEG;AACH,eAAO,MAAM,kBAAkB,2CAO7B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,uBAAuB,6EAEsD,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CircuitBreaker Service
|
|
3
|
+
*
|
|
4
|
+
* Implements the circuit breaker pattern to prevent cascading failures
|
|
5
|
+
* when the Docker daemon becomes unresponsive or overloaded.
|
|
6
|
+
*
|
|
7
|
+
* States:
|
|
8
|
+
* - Closed: Normal operation, requests pass through
|
|
9
|
+
* - Open: Circuit tripped, requests fail fast
|
|
10
|
+
* - HalfOpen: Testing if the service has recovered
|
|
11
|
+
*/
|
|
12
|
+
import { Context, Effect, Layer, Ref, Duration, Clock } from 'effect';
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when the circuit is open.
|
|
15
|
+
*/
|
|
16
|
+
export class CircuitOpenError extends Error {
|
|
17
|
+
_tag = 'CircuitOpenError';
|
|
18
|
+
circuitName;
|
|
19
|
+
resetTimeout;
|
|
20
|
+
constructor(circuitName, resetTimeout) {
|
|
21
|
+
super(`Circuit breaker "${circuitName}" is open. ` +
|
|
22
|
+
`Retry after ${Duration.toMillis(resetTimeout)}ms`);
|
|
23
|
+
this.circuitName = circuitName;
|
|
24
|
+
this.resetTimeout = resetTimeout;
|
|
25
|
+
this.name = 'CircuitOpenError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* CircuitBreaker service tag for dependency injection.
|
|
30
|
+
*/
|
|
31
|
+
export class CircuitBreaker extends Context.Tag('@harpoon/CircuitBreaker')() {
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Factory function to create a circuit breaker with specific configuration.
|
|
35
|
+
*/
|
|
36
|
+
export const makeCircuitBreaker = (config) => Effect.gen(function* () {
|
|
37
|
+
const name = config.name ?? 'default';
|
|
38
|
+
const resetTimeoutMs = Duration.toMillis(config.resetTimeout);
|
|
39
|
+
// Initialize state
|
|
40
|
+
const stateRef = yield* Ref.make({
|
|
41
|
+
state: 'Closed',
|
|
42
|
+
failures: 0,
|
|
43
|
+
lastFailure: null,
|
|
44
|
+
successesSinceHalfOpen: 0,
|
|
45
|
+
});
|
|
46
|
+
// Helper to check if we should transition from Open to HalfOpen
|
|
47
|
+
const checkReset = Effect.gen(function* () {
|
|
48
|
+
const current = yield* Ref.get(stateRef);
|
|
49
|
+
if (current.state !== 'Open')
|
|
50
|
+
return;
|
|
51
|
+
const now = yield* Clock.currentTimeMillis;
|
|
52
|
+
const timeSinceFailure = current.lastFailure ? now - current.lastFailure : Infinity;
|
|
53
|
+
if (timeSinceFailure >= resetTimeoutMs) {
|
|
54
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
55
|
+
...s,
|
|
56
|
+
state: 'HalfOpen',
|
|
57
|
+
successesSinceHalfOpen: 0,
|
|
58
|
+
}));
|
|
59
|
+
yield* Effect.logDebug('Circuit breaker transitioning to HalfOpen').pipe(Effect.annotateLogs({ circuit: name }));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Record a failure
|
|
63
|
+
const recordFailure = Effect.gen(function* () {
|
|
64
|
+
const now = yield* Clock.currentTimeMillis;
|
|
65
|
+
const current = yield* Ref.get(stateRef);
|
|
66
|
+
if (current.state === 'HalfOpen') {
|
|
67
|
+
// Any failure in HalfOpen reopens the circuit
|
|
68
|
+
yield* Ref.set(stateRef, {
|
|
69
|
+
state: 'Open',
|
|
70
|
+
failures: current.failures + 1,
|
|
71
|
+
lastFailure: now,
|
|
72
|
+
successesSinceHalfOpen: 0,
|
|
73
|
+
});
|
|
74
|
+
yield* Effect.logWarning('Circuit breaker reopened from HalfOpen').pipe(Effect.annotateLogs({ circuit: name }));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const newFailures = current.failures + 1;
|
|
78
|
+
const shouldOpen = newFailures >= config.maxFailures;
|
|
79
|
+
yield* Ref.set(stateRef, {
|
|
80
|
+
state: shouldOpen ? 'Open' : 'Closed',
|
|
81
|
+
failures: newFailures,
|
|
82
|
+
lastFailure: now,
|
|
83
|
+
successesSinceHalfOpen: 0,
|
|
84
|
+
});
|
|
85
|
+
if (shouldOpen) {
|
|
86
|
+
yield* Effect.logWarning('Circuit breaker opened').pipe(Effect.annotateLogs({
|
|
87
|
+
circuit: name,
|
|
88
|
+
failures: newFailures,
|
|
89
|
+
maxFailures: config.maxFailures,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// Record a success
|
|
95
|
+
const recordSuccess = Effect.gen(function* () {
|
|
96
|
+
const current = yield* Ref.get(stateRef);
|
|
97
|
+
if (current.state === 'HalfOpen') {
|
|
98
|
+
// Success in HalfOpen mode - close the circuit
|
|
99
|
+
yield* Ref.set(stateRef, {
|
|
100
|
+
state: 'Closed',
|
|
101
|
+
failures: 0,
|
|
102
|
+
lastFailure: null,
|
|
103
|
+
successesSinceHalfOpen: 0,
|
|
104
|
+
});
|
|
105
|
+
yield* Effect.logInfo('Circuit breaker closed after successful probe').pipe(Effect.annotateLogs({ circuit: name }));
|
|
106
|
+
}
|
|
107
|
+
else if (current.state === 'Closed' && current.failures > 0) {
|
|
108
|
+
// Reset failure count on success in closed state
|
|
109
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
110
|
+
...s,
|
|
111
|
+
failures: 0,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const service = {
|
|
116
|
+
_tag: 'CircuitBreaker',
|
|
117
|
+
protect: (effect) => Effect.gen(function* () {
|
|
118
|
+
// Check if we should transition from Open to HalfOpen
|
|
119
|
+
yield* checkReset;
|
|
120
|
+
const current = yield* Ref.get(stateRef);
|
|
121
|
+
if (current.state === 'Open') {
|
|
122
|
+
return yield* Effect.fail(new CircuitOpenError(name, config.resetTimeout));
|
|
123
|
+
}
|
|
124
|
+
// Execute the effect
|
|
125
|
+
const result = yield* effect.pipe(Effect.tap(() => recordSuccess), Effect.tapError(() => recordFailure));
|
|
126
|
+
return result;
|
|
127
|
+
}),
|
|
128
|
+
getState: () => Ref.get(stateRef).pipe(Effect.map((s) => s.state)),
|
|
129
|
+
reset: () => Ref.set(stateRef, {
|
|
130
|
+
state: 'Closed',
|
|
131
|
+
failures: 0,
|
|
132
|
+
lastFailure: null,
|
|
133
|
+
successesSinceHalfOpen: 0,
|
|
134
|
+
}).pipe(Effect.tap(() => Effect.logInfo('Circuit breaker manually reset').pipe(Effect.annotateLogs({ circuit: name })))),
|
|
135
|
+
getFailures: () => Ref.get(stateRef).pipe(Effect.map((s) => s.failures)),
|
|
136
|
+
};
|
|
137
|
+
return service;
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Default circuit breaker configuration.
|
|
141
|
+
*/
|
|
142
|
+
const defaultConfig = {
|
|
143
|
+
maxFailures: 5,
|
|
144
|
+
resetTimeout: Duration.seconds(30),
|
|
145
|
+
name: 'docker',
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Live implementation with default configuration.
|
|
149
|
+
*/
|
|
150
|
+
export const CircuitBreakerLive = Layer.effect(CircuitBreaker, makeCircuitBreaker(defaultConfig));
|
|
151
|
+
/**
|
|
152
|
+
* Test implementation that never opens (all requests pass through).
|
|
153
|
+
*/
|
|
154
|
+
export const CircuitBreakerTest = Layer.succeed(CircuitBreaker, {
|
|
155
|
+
_tag: 'CircuitBreaker',
|
|
156
|
+
protect: (effect) => effect,
|
|
157
|
+
getState: () => Effect.succeed('Closed'),
|
|
158
|
+
reset: () => Effect.void,
|
|
159
|
+
getFailures: () => Effect.succeed(0),
|
|
160
|
+
});
|
|
161
|
+
/**
|
|
162
|
+
* Create a custom circuit breaker layer with specific configuration.
|
|
163
|
+
*/
|
|
164
|
+
export const makeCircuitBreakerLayer = (config) => Layer.effect(CircuitBreaker, makeCircuitBreaker(config));
|