|
1 | 1 | import {TemplateContext} from "../../templates/template-context.js"; |
2 | | -import {MappingToken, SequenceToken, StringToken, TemplateToken} from "../../templates/tokens/index.js"; |
| 2 | +import {StringToken, TemplateToken} from "../../templates/tokens/index.js"; |
3 | 3 | import {isString} from "../../templates/tokens/type-guards.js"; |
4 | | -import {Container, Credential} from "../workflow-template.js"; |
5 | | - |
6 | | -export function convertToJobContainer(context: TemplateContext, container: TemplateToken): Container | undefined { |
7 | | - let image: StringToken | undefined; |
8 | | - let env: MappingToken | undefined; |
9 | | - let ports: SequenceToken | undefined; |
10 | | - let volumes: SequenceToken | undefined; |
11 | | - let options: StringToken | undefined; |
12 | | - |
13 | | - // Skip validation for expressions for now to match |
14 | | - // behavior of the other parsers |
15 | | - for (const [, token] of TemplateToken.traverse(container)) { |
16 | | - if (token.isExpression) { |
17 | | - return; |
18 | | - } |
| 4 | +import {Container} from "../workflow-template.js"; |
| 5 | + |
| 6 | +const DOCKER_URI_PREFIX = "docker://"; |
| 7 | + |
| 8 | +export function convertToJobContainer( |
| 9 | + context: TemplateContext, |
| 10 | + container: TemplateToken, |
| 11 | + isServiceContainer = false |
| 12 | +): Container | undefined { |
| 13 | + // Expression? Skip validation |
| 14 | + if (container.isExpression) { |
| 15 | + return; |
19 | 16 | } |
20 | 17 |
|
| 18 | + // Shorthand form |
21 | 19 | if (isString(container)) { |
22 | | - // Workflow uses shorthand syntax `container: image-name` |
23 | | - image = container.assertString("container item"); |
| 20 | + const image = container.assertString("container item"); |
24 | 21 | if (!image || image.value.length === 0) { |
| 22 | + // Error for service container, silent for job container |
| 23 | + if (isServiceContainer) { |
| 24 | + context.error(container, "Container image cannot be empty"); |
| 25 | + } |
| 26 | + return; |
| 27 | + } |
| 28 | + |
| 29 | + // Trim docker:// prefix and check if empty |
| 30 | + const trimmed = image.value.startsWith(DOCKER_URI_PREFIX) |
| 31 | + ? image.value.substring(DOCKER_URI_PREFIX.length) |
| 32 | + : image.value; |
| 33 | + if (trimmed.length === 0) { |
25 | 34 | context.error(container, "Container image cannot be empty"); |
26 | 35 | return; |
27 | 36 | } |
28 | | - return {image: image}; |
| 37 | + |
| 38 | + return {image}; |
29 | 39 | } |
30 | 40 |
|
| 41 | + // Mapping form |
31 | 42 | const mapping = container.assertMapping("container item"); |
32 | | - if (mapping) |
| 43 | + |
| 44 | + let hasExpressionKey = false; |
| 45 | + let hasImageKey = false; |
| 46 | + |
| 47 | + if (mapping) { |
33 | 48 | for (const item of mapping) { |
| 49 | + // Key is expression? |
| 50 | + if (item.key.isExpression) { |
| 51 | + hasExpressionKey = true; |
| 52 | + continue; |
| 53 | + } |
| 54 | + |
34 | 55 | const key = item.key.assertString("container item key"); |
35 | | - const value = item.value; |
36 | | - |
37 | | - switch (key.value) { |
38 | | - case "image": |
39 | | - image = value.assertString("container image"); |
40 | | - break; |
41 | | - case "credentials": |
42 | | - convertToJobCredentials(context, value); |
43 | | - break; |
44 | | - case "env": |
45 | | - env = value.assertMapping("container env"); |
46 | | - for (const envItem of env) { |
47 | | - envItem.key.assertString("container env value"); |
48 | | - } |
49 | | - break; |
50 | | - case "ports": |
51 | | - ports = value.assertSequence("container ports"); |
52 | | - for (const port of ports) { |
53 | | - port.assertString("container port"); |
54 | | - } |
55 | | - break; |
56 | | - case "volumes": |
57 | | - volumes = value.assertSequence("container volumes"); |
58 | | - for (const volume of volumes) { |
59 | | - volume.assertString("container volume"); |
| 56 | + |
| 57 | + if (key.value === "image") { |
| 58 | + hasImageKey = true; |
| 59 | + |
| 60 | + // Expression? Skip |
| 61 | + if (item.value.isExpression) { |
| 62 | + continue; |
| 63 | + } |
| 64 | + |
| 65 | + const imageStr = item.value.assertString("container image"); |
| 66 | + if (imageStr) { |
| 67 | + // Trim docker:// prefix and check if empty |
| 68 | + const trimmed = imageStr.value.startsWith(DOCKER_URI_PREFIX) |
| 69 | + ? imageStr.value.substring(DOCKER_URI_PREFIX.length) |
| 70 | + : imageStr.value; |
| 71 | + if (trimmed.length === 0) { |
| 72 | + context.error(item.value, "Container image cannot be empty"); |
60 | 73 | } |
61 | | - break; |
62 | | - case "options": |
63 | | - options = value.assertString("container options"); |
64 | | - break; |
65 | | - default: |
66 | | - context.error(key, `Unexpected container item key: ${key.value}`); |
| 74 | + } |
67 | 75 | } |
68 | 76 | } |
| 77 | + } |
69 | 78 |
|
70 | | - if (!image || image.value.length === 0) { |
| 79 | + // Check for missing image key |
| 80 | + if (!hasImageKey && !hasExpressionKey) { |
71 | 81 | context.error(container, "Container image cannot be empty"); |
72 | | - } else { |
73 | | - return {image, env, ports, volumes, options}; |
74 | 82 | } |
75 | 83 | } |
76 | 84 |
|
77 | 85 | export function convertToJobServices(context: TemplateContext, services: TemplateToken): Container[] | undefined { |
| 86 | + // Expression? Skip validation |
| 87 | + if (services.isExpression) { |
| 88 | + return; |
| 89 | + } |
| 90 | + |
78 | 91 | const serviceList: Container[] = []; |
79 | 92 |
|
80 | 93 | const mapping = services.assertMapping("services"); |
81 | 94 | for (const service of mapping) { |
| 95 | + // Key is expression? |
| 96 | + if (service.key.isExpression) { |
| 97 | + continue; |
| 98 | + } |
| 99 | + |
82 | 100 | service.key.assertString("service key"); |
83 | | - const container = convertToJobContainer(context, service.value); |
| 101 | + const container = convertToJobContainer(context, service.value, true); |
84 | 102 | if (container) { |
85 | 103 | serviceList.push(container); |
86 | 104 | } |
87 | 105 | } |
88 | 106 | return serviceList; |
89 | 107 | } |
90 | | - |
91 | | -function convertToJobCredentials(context: TemplateContext, value: TemplateToken): Credential | undefined { |
92 | | - const mapping = value.assertMapping("credentials"); |
93 | | - |
94 | | - let username: StringToken | undefined; |
95 | | - let password: StringToken | undefined; |
96 | | - |
97 | | - for (const item of mapping) { |
98 | | - const key = item.key.assertString("credentials item"); |
99 | | - const value = item.value; |
100 | | - |
101 | | - switch (key.value) { |
102 | | - case "username": |
103 | | - username = value.assertString("credentials username"); |
104 | | - break; |
105 | | - case "password": |
106 | | - password = value.assertString("credentials password"); |
107 | | - break; |
108 | | - default: |
109 | | - context.error(key, `credentials key ${key.value}`); |
110 | | - } |
111 | | - } |
112 | | - |
113 | | - return {username, password}; |
114 | | -} |
0 commit comments