@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.
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bun test
2
+
3
+ import * as bunTest from 'bun:test'
4
+ import { run } from 't44/standalone-rt'
5
+ import { join } from 'path'
6
+
7
+ const {
8
+ test: { describe, it, expect },
9
+ imageContext,
10
+ } = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
11
+ const spine = await encapsulate({
12
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
13
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
14
+ '#': {
15
+ test: {
16
+ type: CapsulePropertyTypes.Mapping,
17
+ value: 't44/caps/ProjectTest',
18
+ options: {
19
+ '#': {
20
+ bunTest,
21
+ env: {}
22
+ }
23
+ }
24
+ },
25
+ imageContext: {
26
+ type: CapsulePropertyTypes.Mapping,
27
+ value: './ImageContext',
28
+ },
29
+ }
30
+ }
31
+ }, {
32
+ importMeta: import.meta,
33
+ importStack: makeImportStack(),
34
+ capsuleName: '@stream44.studio/t44-docker.com/caps/ImageContext.test'
35
+ })
36
+ return { spine }
37
+ }, async ({ spine, apis }: any) => {
38
+ return apis[spine.capsuleSourceLineRef]
39
+ }, {
40
+ importMeta: import.meta
41
+ })
42
+
43
+ describe('ImageContext Capsule', () => {
44
+
45
+ describe('DOCKERFILE_VARIANTS', () => {
46
+ it('should have correct structure', () => {
47
+ expect(imageContext.DOCKERFILE_VARIANTS).toBeDefined();
48
+ expect(imageContext.DOCKERFILE_VARIANTS.alpine).toBeDefined();
49
+ expect(imageContext.DOCKERFILE_VARIANTS.distroless).toBeDefined();
50
+ expect(imageContext.DOCKERFILE_VARIANTS.alpine.dockerfile).toBe('Dockerfile.alpine');
51
+ expect(imageContext.DOCKERFILE_VARIANTS.alpine.tagSuffix).toBe('alpine');
52
+ expect(imageContext.DOCKERFILE_VARIANTS.distroless.dockerfile).toBe('Dockerfile.distroless');
53
+ });
54
+ });
55
+
56
+ describe('getImageTag', () => {
57
+ it('should compute correct image tag', () => {
58
+ imageContext.organization = 'test-org';
59
+ imageContext.repository = 'test-repo';
60
+ const tag = imageContext.getImageTag({ variant: 'alpine', arch: 'linux-arm64' });
61
+ expect(tag).toBe('test-org/test-repo:alpine-arm64');
62
+ });
63
+
64
+ it('should compute tag for x64', () => {
65
+ imageContext.organization = 'test-org';
66
+ imageContext.repository = 'test-repo';
67
+ const tag = imageContext.getImageTag({ variant: 'alpine', arch: 'linux-x64' });
68
+ expect(tag).toBe('test-org/test-repo:alpine-amd64');
69
+ });
70
+
71
+ it('should throw without variant/arch', () => {
72
+ imageContext.variant = undefined;
73
+ imageContext.arch = undefined;
74
+ expect(() => imageContext.getImageTag()).toThrow('variant and arch must be set');
75
+ });
76
+
77
+ it('should use instance variant/arch when not passed as opts', () => {
78
+ imageContext.organization = 'test-org';
79
+ imageContext.repository = 'test-repo';
80
+ imageContext.variant = 'alpine';
81
+ imageContext.arch = 'linux-arm64';
82
+ const tag = imageContext.getImageTag();
83
+ expect(tag).toBe('test-org/test-repo:alpine-arm64');
84
+ });
85
+ });
86
+
87
+ describe('getLatestImageTag', () => {
88
+ it('should append -latest suffix', () => {
89
+ imageContext.organization = 'test-org';
90
+ imageContext.repository = 'test-repo';
91
+ const tag = imageContext.getLatestImageTag({ variant: 'alpine', arch: 'linux-arm64' });
92
+ expect(tag).toBe('test-org/test-repo:alpine-arm64-latest');
93
+ });
94
+ });
95
+
96
+ describe('getBuildContextDir', () => {
97
+ it('should compute correct build context dir from buildContextBaseDir', () => {
98
+ imageContext.buildContextBaseDir = '/tmp/build';
99
+ const dir = imageContext.getBuildContextDir({ variant: 'alpine' });
100
+ expect(dir).toBe('/tmp/build/alpine');
101
+ });
102
+
103
+ it('should derive buildContextBaseDir from appBaseDir when not set', () => {
104
+ imageContext.buildContextBaseDir = '';
105
+ imageContext.appBaseDir = '/tmp/myapp';
106
+ const dir = imageContext.getBuildContextDir({ variant: 'alpine' });
107
+ expect(dir).toBe(join('/tmp/myapp', '.~o/t44-docker.com', 'alpine'));
108
+ });
109
+
110
+ it('should throw without variant', () => {
111
+ imageContext.variant = undefined;
112
+ expect(() => imageContext.getBuildContextDir()).toThrow('variant must be set');
113
+ });
114
+
115
+ it('should use instance variant when not passed as opts', () => {
116
+ imageContext.buildContextBaseDir = '/tmp/build';
117
+ imageContext.variant = 'distroless';
118
+ const dir = imageContext.getBuildContextDir();
119
+ expect(dir).toBe('/tmp/build/distroless');
120
+ });
121
+ });
122
+
123
+ describe('templateDir', () => {
124
+ it('should default to the bundled tpl directory', () => {
125
+ expect(imageContext.templateDir).toBeTruthy();
126
+ expect(imageContext.templateDir).toContain('Image');
127
+ expect(imageContext.templateDir).toContain('tpl');
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,126 @@
1
+ import { join } from 'path'
2
+
3
+ const DEFAULT_TEMPLATE_DIR = join(__dirname, 'Image', 'tpl');
4
+
5
+ const DOCKERFILE_VARIANTS = {
6
+ alpine: { dockerfile: 'Dockerfile.alpine', tagSuffix: 'alpine', variantDir: 'alpine', variant: 'alpine' },
7
+ distroless: { dockerfile: 'Dockerfile.distroless', tagSuffix: 'distroless', variantDir: 'distroless', variant: 'distroless' },
8
+ } as const;
9
+
10
+ export async function capsule({
11
+ encapsulate,
12
+ CapsulePropertyTypes,
13
+ makeImportStack
14
+ }: {
15
+ encapsulate: any
16
+ CapsulePropertyTypes: any
17
+ makeImportStack: any
18
+ }) {
19
+
20
+ return encapsulate({
21
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
22
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
23
+ '#': {
24
+ cli: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: './Cli',
27
+ },
28
+
29
+ verbose: {
30
+ type: CapsulePropertyTypes.Literal,
31
+ value: false,
32
+ },
33
+ organization: {
34
+ type: CapsulePropertyTypes.Literal,
35
+ value: '' as string,
36
+ },
37
+ repository: {
38
+ type: CapsulePropertyTypes.Literal,
39
+ value: '' as string,
40
+ },
41
+ appBaseDir: {
42
+ type: CapsulePropertyTypes.Literal,
43
+ value: '' as string,
44
+ },
45
+ buildContextBaseDir: {
46
+ type: CapsulePropertyTypes.Literal,
47
+ value: '' as string,
48
+ },
49
+ templateDir: {
50
+ type: CapsulePropertyTypes.Literal,
51
+ value: DEFAULT_TEMPLATE_DIR as string,
52
+ },
53
+ variant: {
54
+ type: CapsulePropertyTypes.Literal,
55
+ value: undefined as string | undefined,
56
+ },
57
+ arch: {
58
+ type: CapsulePropertyTypes.Literal,
59
+ value: undefined as string | undefined,
60
+ },
61
+ buildScriptName: {
62
+ type: CapsulePropertyTypes.Literal,
63
+ value: undefined as string | undefined,
64
+ },
65
+ files: {
66
+ type: CapsulePropertyTypes.Literal,
67
+ value: undefined as Record<string, any> | undefined,
68
+ },
69
+ attestations: {
70
+ type: CapsulePropertyTypes.Literal,
71
+ value: undefined as { sbom?: boolean; provenance?: boolean } | undefined,
72
+ },
73
+ tagLatest: {
74
+ type: CapsulePropertyTypes.Literal,
75
+ value: false,
76
+ },
77
+
78
+ DOCKERFILE_VARIANTS: {
79
+ type: CapsulePropertyTypes.Constant,
80
+ value: DOCKERFILE_VARIANTS,
81
+ },
82
+
83
+ /**
84
+ * Compute image tag from org/repo/variant/arch
85
+ */
86
+ getImageTag: {
87
+ type: CapsulePropertyTypes.Function,
88
+ value: function (this: any, opts?: { variant?: string; arch?: string }): string {
89
+ const variant = opts?.variant ?? this.variant;
90
+ const arch = opts?.arch ?? this.arch;
91
+ if (!variant || !arch) {
92
+ throw new Error('variant and arch must be set to get image tag');
93
+ }
94
+ const variantInfo = DOCKERFILE_VARIANTS[variant as keyof typeof DOCKERFILE_VARIANTS];
95
+ const archInfo = this.cli.DOCKER_ARCHS[arch as keyof typeof this.cli.DOCKER_ARCHS];
96
+ return `${this.organization}/${this.repository}:${variantInfo.tagSuffix}-${archInfo.arch}`;
97
+ }
98
+ },
99
+
100
+ getLatestImageTag: {
101
+ type: CapsulePropertyTypes.Function,
102
+ value: function (this: any, opts?: { variant?: string; arch?: string }): string {
103
+ return `${this.getImageTag(opts)}-latest`;
104
+ }
105
+ },
106
+
107
+ getBuildContextDir: {
108
+ type: CapsulePropertyTypes.Function,
109
+ value: function (this: any, opts?: { variant?: string }): string {
110
+ const variant = opts?.variant ?? this.variant;
111
+ if (!variant) {
112
+ throw new Error('variant must be set to get build context directory');
113
+ }
114
+ const effectiveBuildContextBaseDir = this.buildContextBaseDir
115
+ || (this.appBaseDir ? join(this.appBaseDir, '.~o/t44-docker.com') : '');
116
+ return join(effectiveBuildContextBaseDir, DOCKERFILE_VARIANTS[variant as keyof typeof DOCKERFILE_VARIANTS].variantDir);
117
+ }
118
+ },
119
+ }
120
+ }
121
+ }, {
122
+ importMeta: import.meta,
123
+ importStack: makeImportStack(),
124
+ capsuleName: '@stream44.studio/t44-docker.com/caps/ImageContext',
125
+ })
126
+ }
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env bun test --timeout 180000
2
+
3
+ import * as bunTest from 'bun:test'
4
+ import { describe, it, expect } from 'bun:test'
5
+ import { run } from 't44/standalone-rt'
6
+ import { join, basename } from 'path'
7
+ import { mkdir, writeFile } from 'fs/promises'
8
+
9
+ const { test: { workbenchDir } } = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
10
+ const spine = await encapsulate({
11
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
12
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
13
+ '#': {
14
+ test: {
15
+ type: CapsulePropertyTypes.Mapping,
16
+ value: 't44/caps/ProjectTest',
17
+ options: { '#': { bunTest, env: {} } }
18
+ },
19
+ }
20
+ }
21
+ }, {
22
+ importMeta: import.meta,
23
+ importStack: makeImportStack(),
24
+ capsuleName: '@stream44.studio/t44-docker.com/caps/Project.test'
25
+ })
26
+ return { spine }
27
+ }, async ({ spine, apis }: any) => {
28
+ return apis[spine.capsuleSourceLineRef]
29
+ }, { importMeta: import.meta })
30
+
31
+ async function createSampleApp(baseDir: string): Promise<void> {
32
+ await mkdir(baseDir, { recursive: true });
33
+
34
+ await writeFile(join(baseDir, 'index.ts'), `
35
+ const server = Bun.serve({
36
+ port: 3000,
37
+ fetch(req) {
38
+ return new Response("Hello from project test!");
39
+ },
40
+ });
41
+ console.log("READY");
42
+ console.log(\`Server running on port \${server.port}\`);
43
+ `);
44
+
45
+ await writeFile(join(baseDir, 'package.json'), JSON.stringify({
46
+ name: 'project-test-app',
47
+ version: '0.1.0',
48
+ scripts: { start: 'bun run index.ts' }
49
+ }, null, 2));
50
+ }
51
+
52
+ describe('Project Capsule', () => {
53
+
54
+ describe('getDevelopmentContainerConfig', () => {
55
+ it('should return correct container config', async () => {
56
+ const config = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
57
+ const spine = await encapsulate({
58
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
59
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
60
+ '#': {
61
+ project: {
62
+ type: CapsulePropertyTypes.Mapping,
63
+ value: './Project',
64
+ options: {
65
+ '@stream44.studio/t44-docker.com/caps/ImageContext': {
66
+ '#': {
67
+ organization: 'test-docker-com',
68
+ repository: 'project-config-test',
69
+ appBaseDir: '/tmp/test',
70
+ verbose: false
71
+ },
72
+ },
73
+ '@stream44.studio/t44-docker.com/caps/ContainerContext': {
74
+ '#': {
75
+ ports: [{ internal: 3000, external: 4000 }],
76
+ },
77
+ },
78
+ },
79
+ },
80
+ }
81
+ }
82
+ }, { importMeta: import.meta, importStack: makeImportStack(), capsuleName: '@stream44.studio/t44-docker.com/caps/Project.test.config' })
83
+ return { spine }
84
+ }, async ({ spine, apis }: any) => {
85
+ return apis[spine.capsuleSourceLineRef].project.getDevelopmentContainerConfig()
86
+ }, { importMeta: import.meta, runFromSnapshot: false })
87
+
88
+ expect(config.image).toContain('test-docker-com/project-config-test');
89
+ expect(config.ports).toContainEqual({ internal: 3000, external: 4000 });
90
+ expect(config.name).toContain('dev');
91
+ expect(config.waitFor).toBe('READY');
92
+ expect(config.detach).toBe(true);
93
+ });
94
+
95
+ it('should merge runConfig env', async () => {
96
+ const config = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
97
+ const spine = await encapsulate({
98
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
99
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
100
+ '#': {
101
+ project: {
102
+ type: CapsulePropertyTypes.Mapping,
103
+ value: './Project',
104
+ options: {
105
+ '@stream44.studio/t44-docker.com/caps/ImageContext': {
106
+ '#': {
107
+ organization: 'test-docker-com',
108
+ repository: 'project-env-test',
109
+ appBaseDir: '/tmp/test',
110
+ verbose: false
111
+ },
112
+ },
113
+ '@stream44.studio/t44-docker.com/caps/ContainerContext': {
114
+ '#': {
115
+ ports: [{ internal: 3000, external: 4000 }],
116
+ env: {
117
+ MY_VAR: 'my-value',
118
+ DEBUG: 'true'
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ }
125
+ }
126
+ }, { importMeta: import.meta, importStack: makeImportStack(), capsuleName: '@stream44.studio/t44-docker.com/caps/Project.test.env' })
127
+ return { spine }
128
+ }, async ({ spine, apis }: any) => {
129
+ return apis[spine.capsuleSourceLineRef].project.getDevelopmentContainerConfig()
130
+ }, { importMeta: import.meta, runFromSnapshot: false })
131
+
132
+ expect(config.env.MY_VAR).toBe('my-value');
133
+ expect(config.env.DEBUG).toBe('true');
134
+ });
135
+ });
136
+
137
+ describe('buildDev + runDev + stopDev', () => {
138
+ it('should build, run, verify, and stop a dev container', async () => {
139
+ const appDir = join(workbenchDir, 'project-full-test');
140
+ await createSampleApp(appDir);
141
+
142
+ await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
143
+ const spine = await encapsulate({
144
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
145
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
146
+ '#': {
147
+ project: {
148
+ type: CapsulePropertyTypes.Mapping,
149
+ value: './Project',
150
+ options: {
151
+ '@stream44.studio/t44-docker.com/caps/ContainerContext': {
152
+ '#': {
153
+ ports: [{ internal: 3000, external: 13581 }],
154
+ },
155
+ },
156
+ '@stream44.studio/t44-docker.com/caps/ImageContext': {
157
+ '#': {
158
+ organization: 'test-docker-com',
159
+ repository: 'project-full-test',
160
+ verbose: false
161
+ },
162
+ },
163
+ },
164
+ },
165
+ }
166
+ }
167
+ }, { importMeta: import.meta, importStack: makeImportStack(), capsuleName: '@stream44.studio/t44-docker.com/caps/Project.test.full' })
168
+ return { spine }
169
+ }, async ({ spine, apis }: any) => {
170
+ const project = apis[spine.capsuleSourceLineRef].project
171
+
172
+ project.image.context.appBaseDir = appDir;
173
+ project.image.context.buildContextBaseDir = join(appDir, '.~o/t44-docker.com');
174
+
175
+ const buildResult = await project.buildDev();
176
+ expect(buildResult.imageTag).toBeTruthy();
177
+ expect(buildResult.imageTag).toContain('test-docker-com/project-full-test');
178
+
179
+ const { containerId, stop, ensureRunning } = await project.runDev();
180
+ expect(containerId).toBeTruthy();
181
+
182
+ const isRunning = await ensureRunning();
183
+ expect(isRunning).toBe(true);
184
+
185
+ await stop();
186
+ await project.image.removeImage({ image: buildResult.imageTag, force: true }).catch(() => { });
187
+ }, { importMeta: import.meta, runFromSnapshot: false })
188
+ }, 180000);
189
+ });
190
+
191
+ describe('retagImages', () => {
192
+ it('should retag images from source to target org/repo', async () => {
193
+ const appDir = join(workbenchDir, 'project-retag-test');
194
+ await createSampleApp(appDir);
195
+
196
+ await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
197
+ const sourceSpine = await encapsulate({
198
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
199
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
200
+ '#': {
201
+ project: {
202
+ type: CapsulePropertyTypes.Mapping,
203
+ value: './Project',
204
+ options: {
205
+ '@stream44.studio/t44-docker.com/caps/ImageContext': {
206
+ '#': {
207
+ organization: 'test-docker-com',
208
+ repository: 'project-retag-source',
209
+ verbose: false
210
+ },
211
+ },
212
+ },
213
+ },
214
+ }
215
+ }
216
+ }, { importMeta: import.meta, importStack: makeImportStack(), capsuleName: '@stream44.studio/t44-docker.com/caps/Project.test.retag-source' })
217
+
218
+ const targetSpine = await encapsulate({
219
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
220
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
221
+ '#': {
222
+ project: {
223
+ type: CapsulePropertyTypes.Mapping,
224
+ value: './Project',
225
+ options: {
226
+ '@stream44.studio/t44-docker.com/caps/ImageContext': {
227
+ '#': {
228
+ organization: 'test-docker-com',
229
+ repository: 'project-retag-target',
230
+ verbose: false
231
+ },
232
+ },
233
+ },
234
+ },
235
+ }
236
+ }
237
+ }, { importMeta: import.meta, importStack: makeImportStack(), capsuleName: '@stream44.studio/t44-docker.com/caps/Project.test.retag-target' })
238
+
239
+ return { sourceSpine, targetSpine }
240
+ }, async ({ sourceSpine, targetSpine, apis }: any) => {
241
+ const sourceProject = apis[sourceSpine.capsuleSourceLineRef].project
242
+ const targetProject = apis[targetSpine.capsuleSourceLineRef].project
243
+
244
+ sourceProject.image.context.appBaseDir = appDir;
245
+ sourceProject.image.context.buildContextBaseDir = join(appDir, '.~o/t44-docker.com');
246
+ targetProject.image.context.appBaseDir = appDir;
247
+ targetProject.image.context.buildContextBaseDir = join(appDir, '.~o/t44-docker.com');
248
+
249
+ const currentArch = sourceProject.cli.getCurrentPlatformArch();
250
+ await sourceProject.image.buildVariant({ variant: 'alpine', arch: currentArch });
251
+
252
+ await targetProject.retagImages({ organization: 'test-docker-com', repository: 'project-retag-source' });
253
+
254
+ const targetTags = await targetProject.image.getTags({ organization: 'test-docker-com', repository: 'project-retag-target' });
255
+ expect(targetTags.length).toBeGreaterThan(0);
256
+
257
+ for (const tag of targetTags) {
258
+ await targetProject.image.removeImage({ image: tag.tag, force: true }).catch(() => { });
259
+ }
260
+ const sourceTags = await sourceProject.image.getTags({ organization: 'test-docker-com', repository: 'project-retag-source' });
261
+ for (const tag of sourceTags) {
262
+ await sourceProject.image.removeImage({ image: tag.tag, force: true }).catch(() => { });
263
+ }
264
+ }, { importMeta: import.meta, runFromSnapshot: false })
265
+ }, 120000);
266
+ });
267
+ });