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 |