@stream44.studio/t44-docker.com 0.1.0-rc.3
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/.dco-signatures +9 -0
- package/.github/workflows/dco.yaml +12 -0
- package/.github/workflows/gordian-open-integrity.yaml +13 -0
- package/.github/workflows/test.yaml +29 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +21 -0
- package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
- package/.repo-identifier +1 -0
- package/DCO.md +34 -0
- package/LICENSE.txt +23 -0
- package/README.md +73 -0
- package/caps/Cli.test.ts +116 -0
- package/caps/Cli.ts +134 -0
- package/caps/Container.test.ts +168 -0
- package/caps/Container.ts +484 -0
- package/caps/ContainerContext.test.ts +78 -0
- package/caps/ContainerContext.ts +111 -0
- package/caps/Containers.test.ts +59 -0
- package/caps/Containers.ts +47 -0
- package/caps/Hub.test.ts +107 -0
- package/caps/Hub.ts +367 -0
- package/caps/Image/tpl/Dockerfile.alpine +40 -0
- package/caps/Image/tpl/Dockerfile.distroless +45 -0
- package/caps/Image/tpl/package.json +8 -0
- package/caps/Image.test.ts +269 -0
- package/caps/Image.ts +623 -0
- package/caps/ImageContext.test.ts +130 -0
- package/caps/ImageContext.ts +126 -0
- package/caps/Project.test.ts +267 -0
- package/caps/Project.ts +304 -0
- package/lib/waitForFetch.ts +95 -0
- package/package.json +19 -0
- package/structs/Hub/WorkspaceConnectionConfig.ts +53 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env bun test --timeout 120000
|
|
2
|
+
|
|
3
|
+
import * as bunTest from 'bun:test'
|
|
4
|
+
import { run } from 't44/standalone-rt'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { mkdir, writeFile } from 'fs/promises'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
test: { describe, it, expect, workbenchDir },
|
|
11
|
+
image,
|
|
12
|
+
} = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
|
|
13
|
+
const spine = await encapsulate({
|
|
14
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
15
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
16
|
+
'#': {
|
|
17
|
+
test: {
|
|
18
|
+
type: CapsulePropertyTypes.Mapping,
|
|
19
|
+
value: 't44/caps/ProjectTest',
|
|
20
|
+
options: {
|
|
21
|
+
'#': {
|
|
22
|
+
bunTest,
|
|
23
|
+
env: {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
image: {
|
|
28
|
+
type: CapsulePropertyTypes.Mapping,
|
|
29
|
+
value: './Image',
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, {
|
|
34
|
+
importMeta: import.meta,
|
|
35
|
+
importStack: makeImportStack(),
|
|
36
|
+
capsuleName: '@stream44.studio/t44-docker.com/caps/Image.test'
|
|
37
|
+
})
|
|
38
|
+
return { spine }
|
|
39
|
+
}, async ({ spine, apis }: any) => {
|
|
40
|
+
return apis[spine.capsuleSourceLineRef]
|
|
41
|
+
}, {
|
|
42
|
+
importMeta: import.meta
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a minimal test app for image building tests
|
|
47
|
+
*/
|
|
48
|
+
async function createSampleApp(baseDir: string): Promise<void> {
|
|
49
|
+
await mkdir(baseDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
await writeFile(join(baseDir, 'index.ts'), `
|
|
52
|
+
const server = Bun.serve({
|
|
53
|
+
port: 3000,
|
|
54
|
+
fetch(req) {
|
|
55
|
+
return new Response("Hello from test app!");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
console.log("READY");
|
|
59
|
+
console.log(\`Server running on port \${server.port}\`);
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
await writeFile(join(baseDir, 'package.json'), JSON.stringify({
|
|
63
|
+
name: 'test-app',
|
|
64
|
+
version: '0.1.0',
|
|
65
|
+
scripts: {
|
|
66
|
+
start: 'bun run index.ts'
|
|
67
|
+
}
|
|
68
|
+
}, null, 2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('Image Capsule', () => {
|
|
72
|
+
|
|
73
|
+
describe('Constants', () => {
|
|
74
|
+
it('DOCKERFILE_VARIANTS should have correct structure', () => {
|
|
75
|
+
expect(image.DOCKERFILE_VARIANTS).toBeDefined();
|
|
76
|
+
expect(image.DOCKERFILE_VARIANTS.alpine).toBeDefined();
|
|
77
|
+
expect(image.DOCKERFILE_VARIANTS.distroless).toBeDefined();
|
|
78
|
+
expect(image.DOCKERFILE_VARIANTS.alpine.dockerfile).toBe('Dockerfile.alpine');
|
|
79
|
+
expect(image.DOCKERFILE_VARIANTS.alpine.tagSuffix).toBe('alpine');
|
|
80
|
+
expect(image.DOCKERFILE_VARIANTS.distroless.dockerfile).toBe('Dockerfile.distroless');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('trimIndentation', () => {
|
|
85
|
+
it('should trim common leading whitespace', () => {
|
|
86
|
+
const input = `
|
|
87
|
+
FROM node:18
|
|
88
|
+
WORKDIR /app
|
|
89
|
+
`;
|
|
90
|
+
const result = image.trimIndentation(input);
|
|
91
|
+
expect(result).toContain('FROM node:18');
|
|
92
|
+
expect(result).toContain('WORKDIR /app');
|
|
93
|
+
// Content lines should not have the original deep indentation
|
|
94
|
+
const fromLine = result.split('\n').find((l: string) => l.includes('FROM'));
|
|
95
|
+
expect(fromLine).toBe('FROM node:18');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle already-trimmed strings', () => {
|
|
99
|
+
const input = 'FROM node:18\nWORKDIR /app';
|
|
100
|
+
expect(image.trimIndentation(input)).toBe(input);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getImageTag (via image.context)', () => {
|
|
105
|
+
it('should compute correct image tag', () => {
|
|
106
|
+
image.context.organization = 'test-org';
|
|
107
|
+
image.context.repository = 'test-repo';
|
|
108
|
+
const tag = image.context.getImageTag({ variant: 'alpine', arch: 'linux-arm64' });
|
|
109
|
+
expect(tag).toBe('test-org/test-repo:alpine-arm64');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should throw without variant/arch', () => {
|
|
113
|
+
image.context.variant = undefined;
|
|
114
|
+
image.context.arch = undefined;
|
|
115
|
+
expect(() => image.context.getImageTag()).toThrow('variant and arch must be set');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getLatestImageTag (via image.context)', () => {
|
|
120
|
+
it('should append -latest to image tag', () => {
|
|
121
|
+
image.context.organization = 'test-org';
|
|
122
|
+
image.context.repository = 'test-repo';
|
|
123
|
+
const tag = image.context.getLatestImageTag({ variant: 'alpine', arch: 'linux-arm64' });
|
|
124
|
+
expect(tag).toBe('test-org/test-repo:alpine-arm64-latest');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('getBuildContextDir (via image.context)', () => {
|
|
129
|
+
it('should compute correct build context dir', () => {
|
|
130
|
+
image.context.buildContextBaseDir = '/tmp/build';
|
|
131
|
+
const dir = image.context.getBuildContextDir({ variant: 'alpine' });
|
|
132
|
+
expect(dir).toBe('/tmp/build/alpine');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should throw without variant', () => {
|
|
136
|
+
image.context.variant = undefined;
|
|
137
|
+
expect(() => image.context.getBuildContextDir()).toThrow('variant must be set');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('buildVariant', () => {
|
|
142
|
+
it('should build a Docker image for current platform', async () => {
|
|
143
|
+
const appDir = join(workbenchDir, 'build-test');
|
|
144
|
+
await createSampleApp(appDir);
|
|
145
|
+
|
|
146
|
+
image.context.organization = 'test-docker-com';
|
|
147
|
+
image.context.repository = 'image-build-test';
|
|
148
|
+
image.context.appBaseDir = appDir;
|
|
149
|
+
image.context.buildContextBaseDir = join(appDir, '.~o/t44-docker.com');
|
|
150
|
+
image.context.verbose = false;
|
|
151
|
+
|
|
152
|
+
const currentArch = image.cli.getCurrentPlatformArch();
|
|
153
|
+
const result = await image.buildVariant({
|
|
154
|
+
variant: 'alpine',
|
|
155
|
+
arch: currentArch,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.imageTag).toBeTruthy();
|
|
159
|
+
expect(result.imageTag).toContain('test-docker-com/image-build-test:alpine-');
|
|
160
|
+
|
|
161
|
+
// Verify image exists
|
|
162
|
+
const inspectResult = await image.inspectImage({ image: result.imageTag });
|
|
163
|
+
expect(inspectResult).toBeTruthy();
|
|
164
|
+
|
|
165
|
+
// Cleanup
|
|
166
|
+
await image.removeImage({ image: result.imageTag, force: true });
|
|
167
|
+
}, 120000);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('getTags', () => {
|
|
171
|
+
it('should list tags for a built image', async () => {
|
|
172
|
+
const appDir = join(workbenchDir, 'tags-test');
|
|
173
|
+
await createSampleApp(appDir);
|
|
174
|
+
|
|
175
|
+
image.context.organization = 'test-docker-com';
|
|
176
|
+
image.context.repository = 'image-tags-test';
|
|
177
|
+
image.context.appBaseDir = appDir;
|
|
178
|
+
image.context.buildContextBaseDir = join(appDir, '.~o/t44-docker.com');
|
|
179
|
+
image.context.verbose = false;
|
|
180
|
+
|
|
181
|
+
const currentArch = image.cli.getCurrentPlatformArch();
|
|
182
|
+
await image.buildVariant({ variant: 'alpine', arch: currentArch });
|
|
183
|
+
|
|
184
|
+
const tags = await image.getTags({
|
|
185
|
+
organization: 'test-docker-com',
|
|
186
|
+
repository: 'image-tags-test',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(tags.length).toBeGreaterThan(0);
|
|
190
|
+
expect(tags[0].tag).toContain('test-docker-com/image-tags-test');
|
|
191
|
+
|
|
192
|
+
// Cleanup
|
|
193
|
+
for (const tag of tags) {
|
|
194
|
+
await image.removeImage({ image: tag.tag, force: true }).catch(() => { });
|
|
195
|
+
}
|
|
196
|
+
}, 120000);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('copySpecifiedFiles', () => {
|
|
200
|
+
it('should copy JSON objects as files', async () => {
|
|
201
|
+
const buildDir = join(workbenchDir, 'copy-json-test');
|
|
202
|
+
const appDir = join(workbenchDir, 'copy-json-app');
|
|
203
|
+
await mkdir(buildDir, { recursive: true });
|
|
204
|
+
await mkdir(appDir, { recursive: true });
|
|
205
|
+
|
|
206
|
+
await image.copySpecifiedFiles({
|
|
207
|
+
filesSpec: {
|
|
208
|
+
'config.json': { port: 3000, env: 'test' },
|
|
209
|
+
},
|
|
210
|
+
appBaseDir: appDir,
|
|
211
|
+
buildContextDir: buildDir,
|
|
212
|
+
archDir: 'linux-arm64',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const configPath = join(buildDir, 'config.json');
|
|
216
|
+
expect(existsSync(configPath)).toBe(true);
|
|
217
|
+
const content = JSON.parse(await Bun.file(configPath).text());
|
|
218
|
+
expect(content.port).toBe(3000);
|
|
219
|
+
expect(content.env).toBe('test');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should write string content to files', async () => {
|
|
223
|
+
const buildDir = join(workbenchDir, 'copy-content-test');
|
|
224
|
+
const appDir = join(workbenchDir, 'copy-content-app');
|
|
225
|
+
await mkdir(buildDir, { recursive: true });
|
|
226
|
+
await mkdir(appDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
await image.copySpecifiedFiles({
|
|
229
|
+
filesSpec: {
|
|
230
|
+
'Dockerfile': `
|
|
231
|
+
FROM node:18
|
|
232
|
+
WORKDIR /app
|
|
233
|
+
`,
|
|
234
|
+
},
|
|
235
|
+
appBaseDir: appDir,
|
|
236
|
+
buildContextDir: buildDir,
|
|
237
|
+
archDir: 'linux-arm64',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const dockerfilePath = join(buildDir, 'Dockerfile');
|
|
241
|
+
expect(existsSync(dockerfilePath)).toBe(true);
|
|
242
|
+
const content = await Bun.file(dockerfilePath).text();
|
|
243
|
+
expect(content).toContain('FROM node:18');
|
|
244
|
+
// Should be trimmed
|
|
245
|
+
expect(content).not.toMatch(/^\s{20,}FROM/m);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should handle callback file specs', async () => {
|
|
249
|
+
const buildDir = join(workbenchDir, 'copy-callback-test');
|
|
250
|
+
const appDir = join(workbenchDir, 'copy-callback-app');
|
|
251
|
+
await mkdir(buildDir, { recursive: true });
|
|
252
|
+
await mkdir(appDir, { recursive: true });
|
|
253
|
+
|
|
254
|
+
await image.copySpecifiedFiles({
|
|
255
|
+
filesSpec: {
|
|
256
|
+
'dynamic-config.json': ({ archDir }: any) => ({ arch: archDir }),
|
|
257
|
+
},
|
|
258
|
+
appBaseDir: appDir,
|
|
259
|
+
buildContextDir: buildDir,
|
|
260
|
+
archDir: 'linux-arm64',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const configPath = join(buildDir, 'dynamic-config.json');
|
|
264
|
+
expect(existsSync(configPath)).toBe(true);
|
|
265
|
+
const content = JSON.parse(await Bun.file(configPath).text());
|
|
266
|
+
expect(content.arch).toBe('linux-arm64');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|