Linux GNU 11.4.0 Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 0.0% 0 / 0 / 111
Functions: -% 0 / 1 / 1
Branches: 0.0% 0 / 0 / 36

OMCompiler/Compiler/Util/ContainerImage.mo
Line Branch Exec Source
1 /*
2 * This file is part of OpenModelica.
3 *
4 * Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC),
5 * c/o Linköpings universitet, Department of Computer and Information Science,
6 * SE-58183 Linköping, Sweden.
7 *
8 * All rights reserved.
9 *
10 * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR
11 * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8.
12 * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
13 * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL
14 * VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
15 *
16 * The OpenModelica software and the OSMC (Open Source Modelica Consortium)
17 * Public License (OSMC-PL) are obtained from OSMC, either from the above
18 * address, from the URLs:
19 * http://www.openmodelica.org or
20 * https://github.com/OpenModelica/ or
21 * http://www.ida.liu.se/projects/OpenModelica,
22 * and in the OpenModelica distribution.
23 *
24 * GNU AGPL version 3 is obtained from:
25 * https://www.gnu.org/licenses/licenses.html#GPL
26 *
27 * This program is distributed WITHOUT ANY WARRANTY; without
28 * even the implied warranty of MERCHANTABILITY or FITNESS
29 * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH
30 * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL.
31 *
32 * See the full OSMC Public License conditions for more details.
33 *
34 */
35
36 encapsulated uniontype ContainerImage
37 "file: ContainerImage.mo
38 package: ContainerImage
39 description: This file contains util functions for working with
40 OCI (Open Container Initiative) containers like Docker images or
41 Podman pods.
42 "
43
44 protected
45 import Error;
46 import JSON;
47 import StringUtil;
48 import System;
49 import Util;
50
51 constant String containerTool = "docker" "Simplify future switch to other container virtualization software, e.g. podman";
52
53 public
54 record CONTAINER_IMAGE
55 "OCI container representing Docker image or Podman pod."
56 Option<String> host "Registry location where the image resides.";
57 Option<String> port "Port number for the registry.";
58 Option<String> namespace "Represents a user or organization.";
59 String repository "Image name, identifies specific image.";
60 Option<String> tag "Identifier to specify a particular version or variant of the image.";
61 Option<String> digest "Digest sha256";
62 end CONTAINER_IMAGE;
63
64 function parseWithArgs
65 "Parse container image reference with arguments string:
66 [[HOST[:PORT]/]NAMESPACE/]REPOSITORY[:TAG] [ARGUMENTS]"
67 input list<String> containerReferenceWithArgs;
68 output ContainerImage image;
69 output List<String> arguments;
70 algorithm
71 ✗ if listEmpty(containerReferenceWithArgs) then
72 ✗ Error.addCompilerError("Failed to parse container image reference with arguments \"" + stringDelimitList(containerReferenceWithArgs, " ") + "\".");
73 ✗ fail();
74 end if;
75
76 ✗ image := parseContainerReference(listHead(containerReferenceWithArgs));
77 ✗ arguments := listRest(containerReferenceWithArgs);
78 end parseWithArgs;
79
80 function parseContainerReference
81 "Parse container image reference string:
82 [[HOST[:PORT]/]NAMESPACE/]REPOSITORY[:TAG]"
83 input String containerReference;
84 output ContainerImage image;
85 algorithm
86 image := match Util.stringSplitAtChar(containerReference, "/")
87 local
88 String host_and_port;
89 String host;
90 Option<String> port;
91 String namespace;
92 String repository_and_tag;
93 String repository;
94 Option<String> tag;
95 case {host_and_port, namespace, repository_and_tag}
96 algorithm
97 ✗ (host, port) := parseContainerHostPort(host_and_port);
98 ✗ (repository, tag) := parseContainerRepository(repository_and_tag);
99 ✗ then CONTAINER_IMAGE(SOME(host), port, SOME(namespace), repository, tag, NONE());
100 case {namespace, repository_and_tag}
101 algorithm
102 ✗ (repository, tag) := parseContainerRepository(repository_and_tag);
103 ✗ then CONTAINER_IMAGE(NONE(), NONE(), SOME(namespace), repository, tag, NONE());
104 case {repository_and_tag}
105 algorithm
106 ✗ (repository, tag) := parseContainerRepository(repository_and_tag);
107 ✗ then CONTAINER_IMAGE(NONE(), NONE(), NONE(), repository, tag, NONE());
108 else
109 algorithm
110 ✗ Error.addCompilerError("Failed to parse container image '" + containerReference + "'.");
111 ✗ then fail();
112 end match;
113 end parseContainerReference;
114
115 function getDigestSha
116 "Get sha256 digest from container registry.
117 Warning: This doesn't ensure that the image wasn't changed after downloading."
118 input output ContainerImage image;
119 protected
120 String imageName = toString(image);
121 String cmd;
122 String manifestFile;
123 JSON manifest;
124 JSON descriptor;
125 JSON digest;
126 String digest_sha256_str;
127 algorithm
128 // Retrieve maifest via docker inspect or podman inspect
129 ✗ manifestFile := image.repository + "_manifest.json";
130 ✗ if System.regularFileExists(manifestFile) then
131 ✗ System.removeFile(manifestFile);
132 end if;
133
134 // TODO: docker manifest inspect is experimental!
135 // See https://docs.docker.com/reference/cli/docker/manifest/inspect/
136 ✗ cmd := ContainerImage.containerTool + " manifest inspect " + quoteForShell(imageName) + " -v";
137 ✗ if System.systemCall(cmd, outFile=manifestFile) <> 0 then
138 ✗ Error.addCompilerError("Failed to retrieve manifest of container image '" + imageName + "'.");
139 ✗ Error.addCompilerNotification(System.readFile(manifestFile) + "\n");
140 ✗ System.removeFile(manifestFile);
141 ✗ fail();
142 end if;
143
144 // Parse manifest JSON
145 // Get Descriptor.digest
146 ✗ manifest := JSON.parseFile(manifestFile);
147 descriptor := match manifest
148 ✗ case JSON.OBJECT() then JSON.getOrDefault(manifest, "Descriptor", JSON.NULL());
149 ✗ case JSON.LIST_OBJECT() then JSON.getOrDefault(manifest, "Descriptor", JSON.NULL());
150 case JSON.ARRAY()
151 algorithm
152 /* A manifest list has one entry per platform, each with the digest of
153 * that platform instead of the digest of the manifest list itself.
154 */
155 ✗ Error.addCompilerError("Container image '" + imageName + "' is a multi-platform image, which isn't supported for cross compilation.");
156 ✗ Error.addCompilerNotification("Use a reference to a single platform image, e.g. one of the images listed by `" +
157 ContainerImage.containerTool + " manifest inspect " + imageName + "`.");
158 ✗ System.removeFile(manifestFile);
159 ✗ then fail();
160 else
161 algorithm
162 ✗ Error.addCompilerError("Failed to retrieve manifest descriptor of container image '" + imageName + "'.");
163 ✗ System.removeFile(manifestFile);
164 ✗ then fail();
165 end match;
166 digest := match descriptor
167 ✗ case JSON.OBJECT() then JSON.getOrDefault(descriptor, "digest", JSON.NULL());
168 ✗ case JSON.LIST_OBJECT() then JSON.getOrDefault(descriptor, "digest", JSON.NULL());
169 else JSON.NULL();
170 end match;
171
172 // Get digest
173 digest_sha256_str := match digest
174 ✗ case JSON.STRING() then digest.str;
175 else algorithm
176 ✗ Error.addCompilerError("Failed to retrieve digest SHA from manifest of container image '" + imageName + "'.");
177 ✗ System.removeFile(manifestFile);
178 ✗ then fail();
179 end match;
180
181 // Sanity check for 256-SHA
182 ✗ if not StringUtil.startsWith(digest_sha256_str, "sha256:") then
183 ✗ Error.addCompilerError("Retrieve digest 256-SHA has unexpected format: '" + digest_sha256_str + "'.");
184 ✗ System.removeFile(manifestFile);
185 ✗ fail();
186 end if;
187
188 ✗ image.digest := SOME(digest_sha256_str);
189
190 ✗ System.removeFile(manifestFile);
191 end getDigestSha;
192
193 function isTrustedOpenModelicaImage
194 "Add compiler warning if container image is not known.
195 Run this function before downloading any container images.
196 Image has to be 'ghcr.io/openmodelica/crossbuild' with a known tag and
197 digest to be trusted."
198 input ContainerImage image;
199 output Boolean isOpenModelicaImage = false "True if image is 'ghcr.io/openmodelica/crossbuild'";
200 output Boolean hasKnownDigest = false "True if image is 'ghcr.io/openmodelica/crossbuild' and digest 256 SHA is known in this version of OMC.";
201 protected
202 Boolean isKnownHost;
203 Boolean isKnownNamespace;
204 Boolean isKnownTag;
205 String host;
206 algorithm
207 // Check host
208 isKnownHost := match image.host
209 case NONE() then false;
210 case SOME("docker.io") then false; // We trust Docker Hub, but don't have an official image stored there.
211 case SOME("ghcr.io") then true;
212 case SOME(host)
213 algorithm
214 ✗ Error.addCompilerWarning("Using container registry \"" + host + "\". Make sure you trust the registry.");
215 then false;
216 end match;
217
218 // Check combination host+namespace
219 isKnownNamespace := match (image.namespace, isKnownHost)
220 case (SOME("openmodelica"), true) then true;
221 case (_, true)
222 algorithm
223 ✗ Error.addCompilerWarning("Container image \"" + toString(image) + "\" is an external image. Make sure you trust the image.");
224 then false;
225 else
226 then false;
227 end match;
228
229 // Check combination host+namespace+repository
230 isOpenModelicaImage := match (image.repository, isKnownNamespace)
231 case ("crossbuild", true) then true;
232 case (_, true)
233 algorithm
234 ✗ Error.addCompilerWarning("Container image \"" + toString(image) + "\" is not a known OpenModelica image.");
235 then false;
236 else
237 then false;
238 end match;
239
240 // Check combination host+namespace+repository+tag
241 isKnownTag := match (image.tag, isOpenModelicaImage)
242 case (SOME("v1.28.0"), true) then true;
243 case (_, true)
244 algorithm
245 ✗ Error.addCompilerWarning("Container image \"" + toString(image) + "\" is not tested for this OpenModelica version.");
246 then false;
247 else
248 then false;
249 end match;
250
251 // Check host+namespace+repository+tag+digest
252 hasKnownDigest := match (image.digest, isKnownTag)
253 local
254 String digest;
255 // https://github.com/OpenModelica/openmodelica-crossbuild/pkgs/container/crossbuild/1293204639?tag=v1.28.0
256 case (SOME("sha256:7f0038259e8de276384dc1a0d7297f947e8f3dc4457c08d5dced32f2e49599d8"), true) then true;
257 case (SOME(digest), true)
258 algorithm
259 ✗ Error.addCompilerWarning("Container image \"" + toString(image) + "\" has unknown digest \"" + digest + "\".");
260 ✗ Error.addCompilerNotification("Check https://github.com/OpenModelica/openmodelica-crossbuild/pkgs/container/crossbuild/ for available cross-build images managed by OpenModelica.");
261 then false;
262 case (NONE(), true)
263 algorithm
264 ✗ Error.addCompilerError("Container image \"" + toString(image) + "\" has no digest. That shouldn't be possible.");
265 ✗ then fail();
266 else
267 then false;
268 end match;
269 end isTrustedOpenModelicaImage;
270
271 function pull
272 "Pull container image."
273 input ContainerImage image;
274 protected
275 String pullLogFile;
276 String imageName = toString(image);
277 String cmd;
278 algorithm
279 ✗ pullLogFile := image.repository + "_pull.log";
280
281 ✗ cmd := ContainerImage.containerTool + " pull " + quoteForShell(imageName);
282 ✗ if System.systemCall(cmd, outFile=pullLogFile) <> 0 then
283 ✗ Error.addCompilerError("Failed to pull container image '" + imageName + "'.");
284 ✗ Error.addCompilerNotification(System.readFile(pullLogFile) + "\n");
285 ✗ System.removeFile(pullLogFile);
286 ✗ fail();
287 end if;
288
289 ✗ Error.addCompilerNotification(System.readFile(pullLogFile) + "\n");
290 ✗ System.removeFile(pullLogFile);
291 end pull;
292
293 function pullCommand
294 "Return the command to download the container image manually."
295 input ContainerImage image;
296 output String cmd = ContainerImage.containerTool + " pull " + quoteForShell(toString(image));
297 end pullCommand;
298
299 function isAvailableLocally
300 "Check if container image is already available on this machine.
301 Uses the digest, if known, to make sure the local image is the exact image
302 that was checked by isTrustedOpenModelicaImage and not some other image
303 that happens to have the same tag."
304 input ContainerImage image;
305 output Boolean isAvailable;
306 protected
307 String inspectLogFile;
308 String imageName = toString(image, useDigest = true);
309 String cmd;
310 algorithm
311 ✗ inspectLogFile := image.repository + "_inspect.log";
312 ✗ if System.regularFileExists(inspectLogFile) then
313 ✗ System.removeFile(inspectLogFile);
314 end if;
315
316 ✗ cmd := ContainerImage.containerTool + " image inspect " + quoteForShell(imageName);
317 ✗ isAvailable := System.systemCall(cmd, outFile=inspectLogFile) == 0;
318
319 ✗ if System.regularFileExists(inspectLogFile) then
320 ✗ System.removeFile(inspectLogFile);
321 end if;
322 end isAvailableLocally;
323
324 function isCosignAvailable
325 "Check if `cosign` from sigstore is available in PATH.
326 Adds a compiler warning if it isn't."
327 output Boolean hasCosign;
328 protected
329 String cosignLogFile = "cosign_version.log";
330 algorithm
331 ✗ if System.regularFileExists(cosignLogFile) then
332 ✗ System.removeFile(cosignLogFile);
333 end if;
334
335 ✗ hasCosign := System.systemCall("cosign version", outFile=cosignLogFile) == 0;
336 ✗ if not hasCosign then
337 ✗ Error.addCompilerWarning("Can't find `cosign` from sigstore in PATH. Signatures of container images can't be verified.");
338 ✗ Error.addCompilerNotification("Install cosign from https://github.com/sigstore/cosign to verify and automatically download container images.");
339 end if;
340
341 ✗ if System.regularFileExists(cosignLogFile) then
342 ✗ System.removeFile(cosignLogFile);
343 end if;
344 end isCosignAvailable;
345
346 function assertSignature
347 "Assert that the signature of the container image is valid.
348 The image is identified by its digest, so this can be checked before the
349 image is downloaded. Fails if the signature can't be verified.
350 Needs `cosign` from sigstore to be in PATH."
351 input ContainerImage image;
352 protected
353 String cosignLogFile;
354 String cmd;
355 String imageName = toString(image);
356 String imageReference = toString(image, useDigest = true);
357 algorithm
358 ✗ cosignLogFile := image.repository + "_signature.log";
359 ✗ if System.regularFileExists(cosignLogFile) then
360 ✗ System.removeFile(cosignLogFile);
361 end if;
362 ✗ if not isCosignAvailable() then
363 ✗ Error.addCompilerError("Can't verify signature of container image '" + imageName + "' without `cosign` from sigstore.");
364 ✗ fail();
365 end if;
366
367 // Verification using cosign
368 ✗ cmd := "cosign verify " + quoteForShell(imageReference) +
369 " --certificate-identity=https://github.com/OpenModelica/openmodelica-crossbuild/.github/workflows/publish.yml@refs/tags/v1.28.0" +
370 " --certificate-oidc-issuer=https://token.actions.githubusercontent.com";
371
372 ✗ System.appendFile(cosignLogFile, cmd + "\n");
373 ✗ if System.systemCall(cmd, outFile=cosignLogFile) <> 0 then
374 ✗ Error.addCompilerError("Failed to verify signature of container image '" + imageName + "'.");
375 ✗ Error.addCompilerNotification(System.readFile(cosignLogFile) + "\n");
376 ✗ System.removeFile(cosignLogFile);
377 ✗ fail();
378 end if;
379
380 ✗ System.removeFile(cosignLogFile);
381 end assertSignature;
382
383 function toString
384 "Return container image as string in format
385 [[HOST[:PORT]/]NAMESPACE/]REPOSITORY[:TAG], or
386 [[HOST[:PORT]/]NAMESPACE/]REPOSITORY[@DIGEST] if useDigest is true and the
387 digest is known."
388 input ContainerImage image;
389 input Boolean useDigest = false "Use digest instead of tag, if available.";
390 output String imageString = "";
391 algorithm
392 ✗ imageString := hostToString(image);
393 ✗ if not imageString == "" then
394 ✗ imageString := imageString + "/";
395 end if;
396
397 ✗ imageString := imageString + nameToString(image);
398
399 ✗ if useDigest and isSome(image.digest) then
400 ✗ imageString := imageString + "@" + Util.getOption(image.digest);
401 elseif isSome(image.tag) then
402 ✗ imageString := imageString + ":" + Util.getOption(image.tag);
403 end if;
404 end toString;
405
406 function nameToString
407 "Return name [NAMESPACE/]REPOSITORY as String."
408 input ContainerImage image;
409 output String name = "";
410 algorithm
411 name := match image
412 local
413 String namespace_str;
414 String repository;
415 case CONTAINER_IMAGE(namespace = SOME(namespace_str), repository = repository)
416 ✗ then namespace_str + "/" + repository;
417 case CONTAINER_IMAGE(namespace = NONE(), repository = repository)
418 then repository;
419 else
420 algorithm
421 ✗ Error.addCompilerError("Failed to get name of image reference.");
422 ✗ then fail();
423 end match;
424 end nameToString;
425
426 protected
427 // Helper functions for parsing
428 function parseContainerHostPort
429 "Parse container host and port string:
430 HOST[:PORT]"
431 input String host_and_port;
432 output String host;
433 output Option<String> port = NONE();
434 algorithm
435 host := match Util.stringSplitAtChar(host_and_port, ":")
436 local
437 String host_str;
438 String port_str;
439 case {host_str, port_str}
440 algorithm
441 port := SOME(port_str);
442 then host_str;
443 case {host_str} then host_str;
444 else
445 algorithm
446 ✗ Error.addCompilerError("Failed to parse container host '" + host_and_port + "'.");
447 ✗ then fail();
448 end match;
449 end parseContainerHostPort;
450
451 function quoteForShell
452 "Single-quote a container reference for the shell. parseContainerReference does not
453 constrain the character set of host, namespace, repository or tag, and the reference
454 comes from the user via the platforms argument, so it must not reach a shell bare."
455 input String str;
456 output String quoted;
457 algorithm
458 // Close the quote, escape the quote character, reopen: the POSIX way, as there is
459 // no escape inside a single-quoted string.
460 ✗ quoted := "'" + System.stringReplace(str, "'", "'\\''") + "'";
461 end quoteForShell;
462
463 function parseContainerRepository
464 "Parse container image repository string:
465 REPOSITORY[:TAG]"
466 input String repositoryString;
467 output String repository;
468 output Option<String> tag;
469 algorithm
470 (repository, tag) := match Util.stringSplitAtChar(repositoryString, ":")
471 local
472 String repository_str;
473 String tag_str;
474 case {repository_str, tag_str} then (repository_str, SOME(tag_str));
475 case {repository_str} then (repository_str, NONE());
476 else
477 algorithm
478 ✗ Error.addCompilerError("Failed to parse container image name '" + repositoryString + "'.");
479 ✗ then fail();
480 end match;
481 end parseContainerRepository;
482
483 // Helper functions for toString
484 function hostToString
485 "Return [HOST[:PORT]] as string."
486 input ContainerImage image;
487 output String hostPortStr = "";
488 algorithm
489 hostPortStr := match image
490 local
491 String hostStr;
492 String portStr;
493 case CONTAINER_IMAGE(host = SOME(hostStr), port = SOME(portStr))
494 ✗ then hostStr + ":" + portStr;
495 case CONTAINER_IMAGE(host = SOME(hostStr), port = NONE())
496 then hostStr;
497 case CONTAINER_IMAGE(host = NONE(), port = SOME(_))
498 algorithm
499 ✗ Error.addCompilerError("Port specified without host.");
500 ✗ then fail();
501 case CONTAINER_IMAGE(host = NONE(), port = NONE())
502 then "";
503 else
504 algorithm
505 ✗ Error.addCompilerError("Failed to get host and port of image reference.");
506 ✗ then fail();
507 end match;
508 end hostToString;
509
510 annotation(__OpenModelica_Interface="util");
511 end ContainerImage;
512