diff --git a/src/nix/modules/composition/images.nix b/src/nix/modules/composition/images.nix index c3c09a3..e4b3912 100644 --- a/src/nix/modules/composition/images.nix +++ b/src/nix/modules/composition/images.nix @@ -13,23 +13,15 @@ let addDetails = serviceName: service: builtins.addErrorContext "while evaluating the image for service ${serviceName}" - (let - inherit (service) build; - in { - imageName = build.imageName or service.image.name; - imageTag = - if build.image.imageTag != "" - then build.image.imageTag - else lib.head (lib.strings.splitString "-" (baseNameOf build.image.outPath)); - } // (if build.image.isExe or false - then { - imageExe = build.image.outPath; + ( + let + imageAttrName = "image${lib.optionalString (service.image.tarball.isExe or false) "Exe"}"; + in + { + inherit (service.image.tarball) imageName imageTag; + ${imageAttrName} = service.image.tarball.outPath; } - else { - image = build.image.outPath; - } - ) - ); + ); in { options = { @@ -40,6 +32,26 @@ in }; }; config = { + assertions = + let + assertionsForRepoTagComponent = component: attrName: + lib.mapAttrsToList + (name: value: { + assertion = lib.types.nonEmptyStr.check value.${attrName}; + message = lib.replaceStrings [ "\n" ] [ " " ] '' + Unable to infer the ${component} of the image associated with + config.services.${name}. Please set + config.services.${name}.image.${attrName} to a non-empty + string. + ''; + }) + serviceImages; + + nameAssertions = assertionsForRepoTagComponent "name" "imageName"; + tagAssertions = assertionsForRepoTagComponent "tag" "imageTag"; + in + nameAssertions ++ tagAssertions; + build.imagesToLoad = lib.attrValues serviceImages; docker-compose.extended.images = config.build.imagesToLoad; }; diff --git a/src/nix/modules/service/image.nix b/src/nix/modules/service/image.nix index a38a1c5..c8c7b3c 100644 --- a/src/nix/modules/service/image.nix +++ b/src/nix/modules/service/image.nix @@ -21,6 +21,7 @@ let bool coercedTo listOf + nonEmptyStr nullOr oneOf package @@ -35,14 +36,42 @@ let (pkgs.writeText "dummy-config.json" (builtins.toJSON config.image.rawConfig)) ]; + # Neither image names nor tags can can be empty strings; setting either to an + # empty string will cause `docker load` to croak with the error message + # "invalid reference format". + fallbackImageRepoTagComponent = component: fallback: + if nonEmptyStr.check component + then component + else fallback; + fallbackImageName = fallbackImageRepoTagComponent config.image.name; + fallbackImageTag = fallbackImageRepoTagComponent config.image.tag; + # Shim for `services..image.tarball` definitions that refer to # arbitrary paths and not `dockerTools`-produced derivations. - dummyImagePackage = outPath: { - inherit outPath; - type = "derivation"; - imageName = config.image.name; - imageTag = if config.image.tag == null then "" else config.image.tag; - }; + dummyImagePackage = outPath: + let + tarballSuffix = ".tar.gz"; + repoTagSeparator = "-"; + baseName = baseNameOf outPath; + baseNameNoExtension = lib.strings.removeSuffix tarballSuffix baseName; + baseNameComponents = lib.strings.splitString repoTagSeparator baseNameNoExtension; + fallbacks = + if ((lib.isStorePath outPath) && (lib.hasSuffix tarballSuffix baseName) && (lib.hasInfix repoTagSeparator baseName)) + then { + imageName = lib.concatStringsSep repoTagSeparator (lib.tail baseNameComponents); + imageTag = lib.head baseNameComponents; + } + else { + imageName = null; + imageTag = null; + }; + in + { + inherit outPath; + type = "derivation"; + imageName = fallbackImageName fallbacks.imageName; + imageTag = fallbackImageTag fallbacks.imageTag; + }; # Type matching the essential attributes of derivations produced by # `dockerTools` builder functions. @@ -99,11 +128,11 @@ let builtImage = buildOrStreamLayeredImage { inherit (config.image) - name tag contents includeStorePaths ; + name = fallbackImageName ("localhost/" + config.service.name); config = config.image.rawConfig; maxLayers = 100; @@ -129,33 +158,6 @@ let in { options = { - build.image = mkOption { - type = nullOr package; - description = '' - Docker image derivation to be `docker load`-ed. - - By default, when `services..image.nixBuild` is enabled, this is - the image produced using `services..image.command`, - `services..image.contents`, and - `services..image.rawConfig`. - ''; - defaultText = lib.literalExample '' - pkgs.dockerTools.buildLayeredImage { - # ... - }; - ''; - internal = true; - }; - build.imageName = mkOption { - type = str; - description = "Derived from `build.image`"; - internal = true; - }; - build.imageTag = mkOption { - type = str; - description = "Derived from `build.image`"; - internal = true; - }; image.nixBuild = mkOption { type = bool; description = '' @@ -169,9 +171,8 @@ in ''; }; image.name = mkOption { - type = str; - default = "localhost/" + config.service.name; - defaultText = lib.literalExpression or lib.literalExample ''"localhost/" + config.service.name''; + type = nullOr str; + default = null; description = '' A human readable name for the Docker image. @@ -264,6 +265,11 @@ in builder functions, or a Docker image tarball at some arbitrary location. + By default, when `services..image.nixBuild` is enabled, this is + the image produced using `services..image.command`, + `services..image.contents`, and + `services..image.rawConfig`. + ::: {.note} Using this option causes Arion to ignore most other options in the {option}`services..image` namespace. The exceptions are @@ -272,35 +278,20 @@ in is not a derivation with the attributes `imageName` and `imageTag`. ::: ''; - example = lib.literalExample or lib.literalExpression '' - let - myimage = pkgs.dockerTools.buildImage { - name = "my-image"; - contents = [ pkgs.coreutils ]; - }; - in - config.services = { - myservice = { - image.tarball = myimage; - # ... - }; - } + example = lib.literalExpression '' + pkgs.dockerTools.buildLayeredImage { + # ... + }; ''; }; }; config = lib.mkMerge [{ - build.image = config.image.tarball; - build.imageName = config.build.image.imageName; - build.imageTag = - if config.build.image.imageTag != "" - then config.build.image.imageTag - else lib.head (lib.strings.splitString "-" (baseNameOf config.build.image.outPath)); image.rawConfig.Cmd = config.image.command; image.nixBuild = lib.mkDefault (priorityIsDefault options.service.image); } ( lib.mkIf (config.service.build.context == null) { - service.image = lib.mkDefault "${config.build.imageName}:${config.build.imageTag}"; + service.image = lib.mkDefault "${config.image.tarball.imageName}:${config.image.tarball.imageTag}"; }) ]; }