packages feed

arion-compose 0.2.1.0 → 0.2.2.0

raw patch · 20 files changed

+663/−415 lines, 20 filessetup-changedPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Arion.DockerCompose: [files] :: Args -> [FilePath]
- Arion.DockerCompose: [otherArgs] :: Args -> [Text]
- Arion.ExtendedInfo: [imageExe] :: Image -> Maybe Text
- Arion.ExtendedInfo: [imageName] :: Image -> Text
- Arion.ExtendedInfo: [imageTag] :: Image -> Text
- Arion.ExtendedInfo: [image] :: Image -> Maybe Text
- Arion.ExtendedInfo: [images] :: ExtendedInfo -> [Image]
- Arion.ExtendedInfo: [projectName] :: ExtendedInfo -> Maybe Text
- Arion.Nix: [evalMode] :: EvaluationArgs -> EvaluationMode
- Arion.Nix: [evalModules] :: EvaluationArgs -> NonEmpty FilePath
- Arion.Nix: [evalPkgs] :: EvaluationArgs -> Text
- Arion.Nix: [evalUid] :: EvaluationArgs -> Int
- Arion.Nix: [evalUserArgs] :: EvaluationArgs -> [Text]
- Arion.Nix: [evalWorkDir] :: EvaluationArgs -> Maybe FilePath
+ Arion.DockerCompose: [$sel:files:Args] :: Args -> [FilePath]
+ Arion.DockerCompose: [$sel:otherArgs:Args] :: Args -> [Text]
+ Arion.ExtendedInfo: [$sel:image:Image] :: Image -> Maybe Text
+ Arion.ExtendedInfo: [$sel:imageExe:Image] :: Image -> Maybe Text
+ Arion.ExtendedInfo: [$sel:imageName:Image] :: Image -> Text
+ Arion.ExtendedInfo: [$sel:imageTag:Image] :: Image -> Text
+ Arion.ExtendedInfo: [$sel:images:ExtendedInfo] :: ExtendedInfo -> [Image]
+ Arion.ExtendedInfo: [$sel:projectName:ExtendedInfo] :: ExtendedInfo -> Maybe Text
+ Arion.Nix: [$sel:evalModulesFile:EvaluationArgs] :: EvaluationArgs -> NonEmpty FilePath
+ Arion.Nix: [$sel:extraNixArgs:EvaluationArgs] :: EvaluationArgs -> [Text]
+ Arion.Nix: [$sel:mode:EvaluationArgs] :: EvaluationArgs -> EvaluationMode
+ Arion.Nix: [$sel:pkgsExpr:EvaluationArgs] :: EvaluationArgs -> Text
+ Arion.Nix: [$sel:posixUID:EvaluationArgs] :: EvaluationArgs -> Int
+ Arion.Nix: [$sel:workDir:EvaluationArgs] :: EvaluationArgs -> Maybe FilePath

Files

CHANGELOG.md view
@@ -1,5 +1,25 @@ # Revision history for Arion +## 0.2.2.0 -- 2024-12-11++### Added++* [`serivces.<name>.service.blkio_config`](https://docs.hercules-ci.com/arion/options#_services_name_service_blkio_config)+* [`services.<name>.service.build.dockerfile`](https://docs.hercules-ci.com/arion/options#_services_name_service_build_dockerfile)+* [`services.<name>.service.stop_grace_period`](https://docs.hercules-ci.com/arion/options#_services_name_service_stop_grace_period)+* In the [NixOS deployment module], `virtualisation.arion.projects.<name>.serviceName` to override `<name>` used in systemd unit++### Fixed++* `nix repl` did not pass `--file` to Nix for file-based repls+* `services.<name>.service.build.context` was ignored+* `boot.tmpOnTmpfs` -> `boot.tmp.useTmpfs`+* Remove lorri from local development environment, as it would fail silently++### Removed++* `defaultPackage` flake output. Use `packages.<system>.default` instead.+ ## 0.2.1.0 -- 2023-07-26  ### Added@@ -98,3 +118,6 @@  * First released version. Released on an unsuspecting world. +++[NixOS deployment module]: https://docs.hercules-ci.com/arion/deployment#_nixos_module
Setup.hs view
@@ -1,2 +1,3 @@ import Distribution.Simple+ main = defaultMain
arion-compose.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.4  name:                arion-compose-version: 0.2.1.0+version: 0.2.2.0 synopsis:            Run docker-compose with help from Nix/NixOS description:         Arion is a tool for building and running applications that consist of multiple docker containers using NixOS modules. It has special support for docker images that are built with Nix, for a smooth development experience and improved performance. homepage:            https://github.com/hercules-ci/arion#readme@@ -44,6 +44,15 @@                    , protolude >= 0.2                    , unix   ghc-options:     -Wall+  default-extensions:+    BlockArguments+    DeriveAnyClass+    DeriveGeneric+    DerivingStrategies+    LambdaCase+    NoFieldSelectors+    OverloadedRecordDot+    OverloadedStrings  flag ghci   default: False
src/haskell/exe/Main.hs view
@@ -1,80 +1,98 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ApplicativeDo #-}-{-# LANGUAGE OverloadedStrings #-}--import Protolude hiding (Down, option)+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoImplicitPrelude #-} -import           Arion.Nix-import           Arion.Aeson-import           Arion.Images (loadImages)+import Arion.Aeson import qualified Arion.DockerCompose as DockerCompose-import           Arion.Services (getDefaultExec)-import           Arion.ExtendedInfo (loadExtendedInfoFromPath, ExtendedInfo(images, projectName))--import Options.Applicative+import Arion.ExtendedInfo (ExtendedInfo (images, projectName), loadExtendedInfoFromPath)+import Arion.Images (loadImages)+import Arion.Nix+import Arion.Services (getDefaultExec) import Control.Monad.Fail-+import Data.Aeson (Value) import qualified Data.Text as T import qualified Data.Text.IO as T--import Data.Aeson(Value)-+import Options.Applicative+import Protolude hiding (Down, option) import System.Posix.User (getRealUserID) -data CommonOptions =-  CommonOptions-    { files :: NonEmpty FilePath-    , pkgs :: Text-    , nixArgs :: [Text]-    , prebuiltComposeFile :: Maybe FilePath-    , noAnsi :: Bool-    , compatibility :: Bool-    , logLevel :: Maybe Text-    }-  deriving (Show)+data CommonOptions = CommonOptions+  { files :: NonEmpty FilePath,+    pkgs :: Text,+    nixArgs :: [Text],+    prebuiltComposeFile :: Maybe FilePath,+    noAnsi :: Bool,+    compatibility :: Bool,+    logLevel :: Maybe Text+  }+  deriving stock (Show) -newtype DockerComposeArgs =-  DockerComposeArgs { unDockerComposeArgs :: [Text] }+newtype DockerComposeArgs = DockerComposeArgs {unDockerComposeArgs :: [Text]}  ensureConfigFile :: [FilePath] -> NonEmpty FilePath-ensureConfigFile []     = "./arion-compose.nix" :| []-ensureConfigFile (x:xs) = x :| xs+ensureConfigFile [] = "./arion-compose.nix" :| []+ensureConfigFile (x : xs) = x :| xs  parseOptions :: Parser CommonOptions parseOptions = do-    files <--      ensureConfigFile <$>-        many (strOption-               (  short 'f'-               <> long "file"-               <> metavar "FILE"-               <> help "Use FILE instead of the default ./arion-compose.nix. \-                        \Can be specified multiple times for a merged configuration" ))-    pkgs <- T.pack <$> strOption-          (  short 'p'-          <> long "pkgs"-          <> metavar "EXPR"-          <> showDefault-          <> value "./arion-pkgs.nix"-          <> help "Use Nix expression EXPR to get the Nixpkgs attrset used for bootstrapping \-                   \and evaluating the configuration." )-    showTrace <- flag False True (long "show-trace"-                    <> help "Causes Nix to print out a stack trace in case of Nix expression evaluation errors. Specify before command.")-    -- TODO --option support (https://github.com/pcapriotti/optparse-applicative/issues/284)-    userNixArgs <- many (T.pack <$> strOption (long "nix-arg" <> metavar "ARG" <> help "Pass an extra argument to nix. Example: --nix-arg --option --nix-arg substitute --nix-arg false"))-    prebuiltComposeFile <- optional $ strOption-               (  long "prebuilt-file"-               <> metavar "JSONFILE"-               <> help "Do not evaluate and use the prebuilt JSONFILE instead. Causes other evaluation-related options to be ignored." )-    noAnsi <- flag False True (long "no-ansi"-                    <> help "Avoid ANSI control sequences")-    compatibility <- flag False True (long "no-ansi"-                    <> help "If set, Docker Compose will attempt to convert deploy keys in v3 files to their non-Swarm equivalent")-    logLevel <- optional $ fmap T.pack $ strOption (long "log-level" <> metavar "LEVEL" <> help "Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")-    pure $-      let nixArgs = userNixArgs <|> "--show-trace" <$ guard showTrace-      in CommonOptions{..}+  files <-+    ensureConfigFile+      <$> many+        ( strOption+            ( short 'f'+                <> long "file"+                <> metavar "FILE"+                <> help+                  "Use FILE instead of the default ./arion-compose.nix. \+                  \Can be specified multiple times for a merged configuration"+            )+        )+  pkgs <-+    T.pack+      <$> strOption+        ( short 'p'+            <> long "pkgs"+            <> metavar "EXPR"+            <> showDefault+            <> value "./arion-pkgs.nix"+            <> help+              "Use Nix expression EXPR to get the Nixpkgs attrset used for bootstrapping \+              \and evaluating the configuration."+        )+  showTrace <-+    flag+      False+      True+      ( long "show-trace"+          <> help "Causes Nix to print out a stack trace in case of Nix expression evaluation errors. Specify before command."+      )+  -- TODO --option support (https://github.com/pcapriotti/optparse-applicative/issues/284)+  userNixArgs <- many (T.pack <$> strOption (long "nix-arg" <> metavar "ARG" <> help "Pass an extra argument to nix. Example: --nix-arg --option --nix-arg substitute --nix-arg false"))+  prebuiltComposeFile <-+    optional $+      strOption+        ( long "prebuilt-file"+            <> metavar "JSONFILE"+            <> help "Do not evaluate and use the prebuilt JSONFILE instead. Causes other evaluation-related options to be ignored."+        )+  noAnsi <-+    flag+      False+      True+      ( long "no-ansi"+          <> help "Avoid ANSI control sequences"+      )+  compatibility <-+    flag+      False+      True+      ( long "no-ansi"+          <> help "If set, Docker Compose will attempt to convert deploy keys in v3 files to their non-Swarm equivalent"+      )+  logLevel <- optional $ fmap T.pack $ strOption (long "log-level" <> metavar "LEVEL" <> help "Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")+  pure $+    let nixArgs = userNixArgs <|> "--show-trace" <$ guard showTrace+     in CommonOptions {..}  textArgument :: Mod ArgumentFields [Char] -> Parser Text textArgument = fmap T.pack . strArgument@@ -82,41 +100,39 @@ parseCommand :: Parser (CommonOptions -> IO ()) parseCommand =   hsubparser-    (    command "cat" (info (pure runCat) (progDesc "Spit out the docker compose file as JSON" <> fullDesc))-      <> command "repl" (info (pure runRepl) (progDesc "Start a nix repl for the whole composition" <> fullDesc))-      <> command "exec" (info (parseExecCommand) (progDesc "Execute a command in a running container" <> fullDesc))-    )-  <|>-  hsubparser-    (    commandDC runBuildAndDC "build" "Build or rebuild services"-      <> commandDC runBuildAndDC "bundle" "Generate a Docker bundle from the Compose file"-      <> commandDC runEvalAndDC "config" "Validate and view the Compose file"-      <> commandDC runBuildAndDC "create" "Create services"-      <> commandDC runEvalAndDC "down" "Stop and remove containers, networks, images, and volumes"-      <> commandDC runEvalAndDC "events" "Receive real time events from containers"-      <> commandDC runDC "help" "Get help on a command"-      <> commandDC runEvalAndDC "images" "List images"-      <> commandDC runEvalAndDC "kill" "Kill containers"-      <> commandDC runEvalAndDC "logs" "View output from containers"-      <> commandDC runEvalAndDC "pause" "Pause services"-      <> commandDC runEvalAndDC "port" "Print the public port for a port binding"-      <> commandDC runEvalAndDC "ps" "List containers"-      <> commandDC runBuildAndDC "pull" "Pull service images"-      <> commandDC runBuildAndDC "push" "Push service images"-      <> commandDC runBuildAndDC "restart" "Restart services"-      <> commandDC runEvalAndDC "rm" "Remove stopped containers"-      <> commandDC runBuildAndDC "run" "Run a one-off command"-      <> commandDC runBuildAndDC "scale" "Set number of containers for a service"-      <> commandDC runBuildAndDC "start" "Start services"-      <> commandDC runEvalAndDC "stop" "Stop services"-      <> commandDC runEvalAndDC "top" "Display the running processes"-      <> commandDC runEvalAndDC "unpause" "Unpause services"-      <> commandDC runBuildAndDC "up" "Create and start containers"-      <> commandDC runDC "version" "Show the Docker-Compose version information"--      <> metavar "DOCKER-COMPOSE-COMMAND"-      <> commandGroup "Docker Compose Commands:"+    ( command "cat" (info (pure runCat) (progDesc "Spit out the docker compose file as JSON" <> fullDesc))+        <> command "repl" (info (pure runRepl) (progDesc "Start a nix repl for the whole composition" <> fullDesc))+        <> command "exec" (info (parseExecCommand) (progDesc "Execute a command in a running container" <> fullDesc))     )+    <|> hsubparser+      ( commandDC runBuildAndDC "build" "Build or rebuild services"+          <> commandDC runBuildAndDC "bundle" "Generate a Docker bundle from the Compose file"+          <> commandDC runEvalAndDC "config" "Validate and view the Compose file"+          <> commandDC runBuildAndDC "create" "Create services"+          <> commandDC runEvalAndDC "down" "Stop and remove containers, networks, images, and volumes"+          <> commandDC runEvalAndDC "events" "Receive real time events from containers"+          <> commandDC runDC "help" "Get help on a command"+          <> commandDC runEvalAndDC "images" "List images"+          <> commandDC runEvalAndDC "kill" "Kill containers"+          <> commandDC runEvalAndDC "logs" "View output from containers"+          <> commandDC runEvalAndDC "pause" "Pause services"+          <> commandDC runEvalAndDC "port" "Print the public port for a port binding"+          <> commandDC runEvalAndDC "ps" "List containers"+          <> commandDC runBuildAndDC "pull" "Pull service images"+          <> commandDC runBuildAndDC "push" "Push service images"+          <> commandDC runBuildAndDC "restart" "Restart services"+          <> commandDC runEvalAndDC "rm" "Remove stopped containers"+          <> commandDC runBuildAndDC "run" "Run a one-off command"+          <> commandDC runBuildAndDC "scale" "Set number of containers for a service"+          <> commandDC runBuildAndDC "start" "Start services"+          <> commandDC runEvalAndDC "stop" "Stop services"+          <> commandDC runEvalAndDC "top" "Display the running processes"+          <> commandDC runEvalAndDC "unpause" "Unpause services"+          <> commandDC runBuildAndDC "up" "Create and start containers"+          <> commandDC runDC "version" "Show the Docker-Compose version information"+          <> metavar "DOCKER-COMPOSE-COMMAND"+          <> commandGroup "Docker Compose Commands:"+      )  parseAll :: Parser (IO ()) parseAll =@@ -124,30 +140,31 @@  parseDockerComposeArgs :: Parser DockerComposeArgs parseDockerComposeArgs =-  DockerComposeArgs <$>-    many (argument (T.pack <$> str) (metavar "DOCKER-COMPOSE ARGS..."))+  DockerComposeArgs+    <$> many (argument (T.pack <$> str) (metavar "DOCKER-COMPOSE ARGS...")) -commandDC-  :: (Text -> DockerComposeArgs -> CommonOptions -> IO ())-  -> Text-  -> Text-  -> Mod CommandFields (CommonOptions -> IO ())+commandDC ::+  (Text -> DockerComposeArgs -> CommonOptions -> IO ()) ->+  Text ->+  Text ->+  Mod CommandFields (CommonOptions -> IO ()) commandDC run cmdStr helpText =   command     (T.unpack cmdStr)-    (info-      (run cmdStr <$> parseDockerComposeArgs)-      (progDesc (T.unpack helpText) <> fullDesc <> forwardOptions))-+    ( info+        (run cmdStr <$> parseDockerComposeArgs)+        (progDesc (T.unpack helpText) <> fullDesc <> forwardOptions)+    )  --------------------------------------------------------------------------------  runDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () runDC cmd (DockerComposeArgs args) _opts = do-  DockerCompose.run DockerCompose.Args-    { files = []-    , otherArgs = [cmd] ++ args-    }+  DockerCompose.run+    DockerCompose.Args+      { files = [],+        otherArgs = [cmd] ++ args+      }  runBuildAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () runBuildAndDC cmd dopts opts = do@@ -160,32 +177,34 @@ callDC :: Text -> DockerComposeArgs -> CommonOptions -> Bool -> FilePath -> IO () callDC cmd dopts opts shouldLoadImages path = do   extendedInfo <- loadExtendedInfoFromPath path-  when shouldLoadImages $ loadImages (images extendedInfo)+  when shouldLoadImages $ loadImages (extendedInfo.images)   let firstOpts = projectArgs extendedInfo <> commonArgs opts-  DockerCompose.run DockerCompose.Args-    { files = [path]-    , otherArgs = firstOpts ++ [cmd] ++ unDockerComposeArgs dopts-    }+  DockerCompose.run+    DockerCompose.Args+      { files = [path],+        otherArgs = firstOpts ++ [cmd] ++ dopts.unDockerComposeArgs+      }  projectArgs :: ExtendedInfo -> [Text] projectArgs extendedInfo =   do-    n <- toList (projectName extendedInfo)+    n <- toList (extendedInfo.projectName)     ["--project-name", n]  commonArgs :: CommonOptions -> [Text]-commonArgs opts = do-    guard (noAnsi opts)+commonArgs opts =+  do+    guard opts.noAnsi     ["--no-ansi"]-  <> do-    guard (compatibility opts)-    ["--compatibility"]-  <> do-    l <- toList (logLevel opts)-    ["--log-level", l]+    <> do+      guard opts.compatibility+      ["--compatibility"]+    <> do+      l <- toList opts.logLevel+      ["--log-level", l]  withBuiltComposeFile :: CommonOptions -> (FilePath -> IO r) -> IO r-withBuiltComposeFile opts cont = case prebuiltComposeFile opts of+withBuiltComposeFile opts cont = case opts.prebuiltComposeFile of   Just prebuilt -> do     cont prebuilt   Nothing -> do@@ -193,7 +212,7 @@     Arion.Nix.withBuiltComposition args cont  withComposeFile :: CommonOptions -> (FilePath -> IO r) -> IO r-withComposeFile opts cont = case prebuiltComposeFile opts of+withComposeFile opts cont = case opts.prebuiltComposeFile of   Just prebuilt -> do     cont prebuilt   Nothing -> do@@ -201,7 +220,7 @@     Arion.Nix.withEvaluatedComposition args cont  getComposeValue :: CommonOptions -> IO Value-getComposeValue opts = case prebuiltComposeFile opts of+getComposeValue opts = case opts.prebuiltComposeFile of   Just prebuilt -> do     decodeFile prebuilt   Nothing -> do@@ -211,14 +230,15 @@ defaultEvaluationArgs :: CommonOptions -> IO EvaluationArgs defaultEvaluationArgs co = do   uid <- getRealUserID-  pure EvaluationArgs-    { evalUid = fromIntegral uid-    , evalModules = files co-    , evalPkgs = pkgs co-    , evalWorkDir = Nothing-    , evalMode = ReadWrite-    , evalUserArgs = nixArgs co-    }+  pure+    EvaluationArgs+      { posixUID = fromIntegral uid,+        evalModulesFile = co.files,+        pkgsExpr = co.pkgs,+        workDir = Nothing,+        mode = ReadWrite,+        extraNixArgs = co.nixArgs+      }  runCat :: CommonOptions -> IO () runCat co = do@@ -250,33 +270,37 @@ noTTYFlag = flag False True (short 'T' <> help "Disable pseudo-tty allocation. By default `exec` allocates a TTY.")  indexOption :: Parser Int-indexOption = option-  (auto >>= \i -> i <$ unless (i >= 1) (fail "container index must be >= 1"))-  (long "index" <> value 1 <> help "Index of the container if there are multiple instances of a service.")+indexOption =+  option+    (auto >>= \i -> i <$ unless (i >= 1) (fail "container index must be >= 1"))+    (long "index" <> value 1 <> help "Index of the container if there are multiple instances of a service.")  envOption :: Parser (Text, Text) envOption = option (auto >>= spl) (long "env" <> short 'e' <> help "Set environment variables (can be used multiple times, not supported in Docker API < 1.25)")-  where spl s = case T.break (== '=') s of-          (_, "") -> fail "--env parameter needs to combine key and value with = sign"-          (k, ev) -> pure (k, T.drop 1 ev)+  where+    spl s = case T.break (== '=') s of+      (_, "") -> fail "--env parameter needs to combine key and value with = sign"+      (k, ev) -> pure (k, T.drop 1 ev)  workdirOption :: Parser Text workdirOption = strOption (long "workdir" <> short 'w' <> metavar "DIR" <> help "Working directory in which to start the command in the container.")  parseExecCommand :: Parser (CommonOptions -> IO ())-parseExecCommand = runExec-  <$> detachFlag-  <*> privilegedFlag-  <*> optional userOption-  <*> noTTYFlag-  <*> indexOption-  <*> many envOption-  <*> optional workdirOption-  <*> textArgument (metavar "SERVICE")-  <*> orEmpty' (-      (:) <$> argument (T.pack <$> str) (metavar "COMMAND")+parseExecCommand =+  runExec+    <$> detachFlag+    <*> privilegedFlag+    <*> optional userOption+    <*> noTTYFlag+    <*> indexOption+    <*> many envOption+    <*> optional workdirOption+    <*> textArgument (metavar "SERVICE")+    <*> orEmpty'+      ( (:)+          <$> argument (T.pack <$> str) (metavar "COMMAND")           <*> many (argument (T.pack <$> str) (metavar "ARG"))-  )+      )  orEmpty' :: (Alternative f, Monoid a) => f a -> f a orEmpty' m = fromMaybe mempty <$> optional m@@ -298,27 +322,28 @@     let commandAndArgs' = case commandAndArgs'' of           [] -> ["/bin/sh"]           x -> x-  -    let args = concat-          [ ["exec"]-          , ("--detach" <$ guard detach :: [Text])-          , "--privileged" <$ guard privileged-          , "-T" <$ guard noTTY-          , (\(k, v) -> ["--env", k <> "=" <> v]) =<< envs-          , join $ toList (user <&> \u -> ["--user", u])-          , ["--index", show index]-          , join $ toList (workDir <&> \w -> ["--workdir", w])-          , [service]-          , commandAndArgs'-          ]-    DockerCompose.run DockerCompose.Args-      { files = [path]-      , otherArgs = projectArgs extendedInfo <> commonArgs opts <> args-      } +    let args =+          concat+            [ ["exec"],+              ("--detach" <$ guard detach :: [Text]),+              "--privileged" <$ guard privileged,+              "-T" <$ guard noTTY,+              (\(k, v) -> ["--env", k <> "=" <> v]) =<< envs,+              join $ toList (user <&> \u -> ["--user", u]),+              ["--index", show index],+              join $ toList (workDir <&> \w -> ["--workdir", w]),+              [service],+              commandAndArgs'+            ]+    DockerCompose.run+      DockerCompose.Args+        { files = [path],+          otherArgs = projectArgs extendedInfo <> commonArgs opts <> args+        }+ main :: IO ()-main = +main =   (join . arionExecParser) (info (parseAll <**> helper) fullDesc)   where     arionExecParser = customExecParser (prefs showHelpOnEmpty)-
src/haskell/lib/Arion/Aeson.hs view
@@ -1,25 +1,27 @@ module Arion.Aeson where -import Prelude ()-import           Data.Aeson-import qualified Data.ByteString.Lazy          as BL-import qualified Data.Text.Lazy                as TL-import qualified Data.Text.Lazy.Builder        as TB+import Data.Aeson+import Data.Aeson.Encode.Pretty+  ( confCompare,+    confTrailingNewline,+    defConfig,+  ) import qualified Data.Aeson.Encode.Pretty-import           Data.Aeson.Encode.Pretty       ( defConfig-                                                , confCompare-                                                , confTrailingNewline-                                                )-import           Protolude+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TB+import Protolude+import Prelude () -pretty :: ToJSON a => a -> Text+pretty :: (ToJSON a) => a -> Text pretty =   TL.toStrict     . TB.toLazyText     . Data.Aeson.Encode.Pretty.encodePrettyToTextBuilder' config-  where config = defConfig { confCompare = compare, confTrailingNewline = True }+  where+    config = defConfig {confCompare = compare, confTrailingNewline = True} -decodeFile :: FromJSON a => FilePath -> IO a+decodeFile :: (FromJSON a) => FilePath -> IO a decodeFile fp = do   b <- BL.readFile fp   case eitherDecode b of
src/haskell/lib/Arion/DockerCompose.hs view
@@ -1,26 +1,24 @@-{-# LANGUAGE OverloadedStrings #-} module Arion.DockerCompose where -import           Prelude                        ( )-import           Protolude-import           System.Process+import Protolude+import System.Process+import Prelude ()  data Args = Args-  { files :: [FilePath]-  , otherArgs :: [Text]+  { files :: [FilePath],+    otherArgs :: [Text]   }  run :: Args -> IO () run args = do-  let fileArgs = files args >>= \f -> ["--file", f]-      allArgs  = fileArgs ++ map toS (otherArgs args)+  let fileArgs = args.files >>= \f -> ["--file", f]+      allArgs = fileArgs ++ map toS args.otherArgs        procSpec = proc "docker-compose" allArgs    -- hPutStrLn stderr ("Running docker-compose with " <> show allArgs :: Text)    withCreateProcess procSpec $ \_in _out _err procHandle -> do-     exitCode <- waitForProcess procHandle      case exitCode of
src/haskell/lib/Arion/ExtendedInfo.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-} {-  Parses the x-arion field in the generated compose file.@@ -8,30 +5,36 @@ -} module Arion.ExtendedInfo where -import Prelude()-import Protolude-import Data.Aeson as Aeson import Arion.Aeson import Control.Lens+import Data.Aeson as Aeson import Data.Aeson.Lens+import Protolude+import Prelude ()  data Image = Image-  { image :: Maybe Text -- ^ image tar.gz file path-  , imageExe :: Maybe Text -- ^ path to exe producing image tar-  , imageName :: Text-  , imageTag :: Text-  } deriving (Eq, Show, Generic, Aeson.ToJSON, Aeson.FromJSON)+  { -- | image tar.gz file path+    image :: Maybe Text,+    -- | path to exe producing image tar+    imageExe :: Maybe Text,+    imageName :: Text,+    imageTag :: Text+  }+  deriving stock (Eq, Show, Generic)+  deriving anyclass (Aeson.ToJSON, Aeson.FromJSON) -data ExtendedInfo = ExtendedInfo {-    projectName :: Maybe Text,+data ExtendedInfo = ExtendedInfo+  { projectName :: Maybe Text,     images :: [Image]-  } deriving (Eq, Show)+  }+  deriving stock (Eq, Show)  loadExtendedInfoFromPath :: FilePath -> IO ExtendedInfo loadExtendedInfoFromPath fp = do   v <- decodeFile fp-  pure ExtendedInfo {-    -- TODO: use aeson derived instance?-    projectName = v ^? key "x-arion" . key "project" . key "name" . _String,-    images = (v :: Aeson.Value) ^.. key "x-arion" . key "images" . _Array . traverse . _JSON-  }+  pure+    ExtendedInfo+      { -- TODO: use aeson derived instance?+        projectName = v ^? key "x-arion" . key "project" . key "name" . _String,+        images = (v :: Aeson.Value) ^.. key "x-arion" . key "images" . _Array . traverse . _JSON+      }
src/haskell/lib/Arion/Images.hs view
@@ -1,54 +1,49 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE OverloadedStrings #-} module Arion.Images-  ( loadImages-  ) where+  ( loadImages,+  )+where -import Prelude()+import Arion.ExtendedInfo (Image (..))+import qualified Data.Text as T import Protolude hiding (to)- import qualified System.Process as Process-import qualified Data.Text as T--import Arion.ExtendedInfo (Image(..))+import Prelude ()  type TaggedImage = Text  -- | Subject to change loadImages :: [Image] -> IO () loadImages requestedImages = do-   loaded <- getDockerImages -  let-    isNew i =-      -- On docker, the image name is unmodified-      (imageName i <> ":" <> imageTag i) `notElem` loaded-      -- On podman, you used to automatically get a localhost prefix-      -- however, since NixOS 22.05, this expected to be part of the name instead-        && ("localhost/" <> imageName i <> ":" <> imageTag i) `notElem` loaded+  let isNew :: Image -> Bool+      isNew i =+        -- On docker, the image name is unmodified+        (i.imageName <> ":" <> i.imageTag) `notElem` loaded+          -- On podman, you used to automatically get a localhost prefix+          -- however, since NixOS 22.05, this expected to be part of the name instead+          && ("localhost/" <> i.imageName <> ":" <> i.imageTag) `notElem` loaded    traverse_ loadImage . filter isNew $ requestedImages  loadImage :: Image -> IO ()-loadImage Image { image = Just imgPath, imageName = name } =+loadImage Image {image = Just imgPath, imageName = name} =   withFile (toS imgPath) ReadMode $ \fileHandle -> do-  let procSpec = (Process.proc "docker" [ "load" ]) {-          Process.std_in = Process.UseHandle fileHandle-        }-  Process.withCreateProcess procSpec $ \_in _out _err procHandle -> do-    e <- Process.waitForProcess procHandle-    case e of-      ExitSuccess -> pass-      ExitFailure code ->-        panic $ "docker load failed with exit code " <> show code <> " for image " <> name <> " from path " <> imgPath--loadImage Image { imageExe = Just imgExe, imageName = name } = do-  let loadSpec = (Process.proc "docker" [ "load" ]) { Process.std_in = Process.CreatePipe }+    let procSpec =+          (Process.proc "docker" ["load"])+            { Process.std_in = Process.UseHandle fileHandle+            }+    Process.withCreateProcess procSpec $ \_in _out _err procHandle -> do+      e <- Process.waitForProcess procHandle+      case e of+        ExitSuccess -> pass+        ExitFailure code ->+          panic $ "docker load failed with exit code " <> show code <> " for image " <> name <> " from path " <> imgPath+loadImage Image {imageExe = Just imgExe, imageName = name} = do+  let loadSpec = (Process.proc "docker" ["load"]) {Process.std_in = Process.CreatePipe}   Process.withCreateProcess loadSpec $ \(Just inHandle) _out _err loadProcHandle -> do     let streamSpec = Process.proc (toS imgExe) []-    Process.withCreateProcess streamSpec { Process.std_out = Process.UseHandle inHandle } $ \_ _ _ streamProcHandle ->+    Process.withCreateProcess streamSpec {Process.std_out = Process.UseHandle inHandle} $ \_ _ _ streamProcHandle ->       withAsync (Process.waitForProcess loadProcHandle) $ \loadExitAsync ->         withAsync (Process.waitForProcess streamProcHandle) $ \streamExitAsync -> do           r <- waitEither loadExitAsync streamExitAsync@@ -61,12 +56,10 @@             ExitFailure code -> panic $ "docker load failed with exit code " <> show code <> " for image " <> name <> " produced by executable " <> imgExe             _ -> pass           pass--loadImage Image { imageName = name } = do+loadImage Image {imageName = name} = do   panic $ "image " <> name <> " doesn't specify an image file or imageExe executable" - getDockerImages :: IO [TaggedImage] getDockerImages = do-  let procSpec = Process.proc "docker" [ "images",  "--filter", "dangling=false", "--format", "{{.Repository}}:{{.Tag}}" ]+  let procSpec = Process.proc "docker" ["images", "--filter", "dangling=false", "--format", "{{.Repository}}:{{.Tag}}"]   map toS . T.lines . toS <$> Process.readCreateProcess procSpec ""
src/haskell/lib/Arion/Nix.hs view
@@ -1,65 +1,64 @@-{-# LANGUAGE OverloadedStrings #-} module Arion.Nix-  ( evaluateComposition-  , withEvaluatedComposition-  , buildComposition-  , withBuiltComposition-  , replForComposition-  , EvaluationArgs(..)-  , EvaluationMode(..)-  ) where+  ( evaluateComposition,+    withEvaluatedComposition,+    buildComposition,+    withBuiltComposition,+    replForComposition,+    EvaluationArgs (..),+    EvaluationMode (..),+  )+where -import           Prelude                        ( )-import           Protolude-import           Arion.Aeson                    ( pretty )-import           Data.Aeson+import Arion.Aeson (pretty)+import Control.Arrow ((>>>))+import Data.Aeson+import qualified Data.ByteString.Lazy as BL+import qualified Data.List.NonEmpty as NE import qualified Data.String-import qualified System.Directory              as Directory-import           System.Process-import qualified Data.ByteString.Lazy          as BL-import           Paths_arion_compose--import qualified Data.Text.IO                  as T--import qualified Data.List.NonEmpty            as NE--import           Control.Arrow                  ( (>>>) )-import           System.IO.Temp                 ( withTempFile )-import           System.IO                      ( hClose )+import qualified Data.Text.IO as T+import Paths_arion_compose+import Protolude+import qualified System.Directory as Directory+import System.IO (hClose)+import System.IO.Temp (withTempFile)+import System.Process+import Prelude () -data EvaluationMode =-  ReadWrite | ReadOnly+data EvaluationMode+  = ReadWrite+  | ReadOnly  data EvaluationArgs = EvaluationArgs- { evalUid :: Int- , evalModules :: NonEmpty FilePath- , evalPkgs :: Text- , evalWorkDir :: Maybe FilePath- , evalMode :: EvaluationMode- , evalUserArgs :: [Text]- }+  { posixUID :: Int,+    evalModulesFile :: NonEmpty FilePath,+    pkgsExpr :: Text,+    workDir :: Maybe FilePath,+    mode :: EvaluationMode,+    extraNixArgs :: [Text]+  }  evaluateComposition :: EvaluationArgs -> IO Value evaluateComposition ea = do   evalComposition <- getEvalCompositionFile   let commandArgs =-        [ "--eval"-        , "--strict"-        , "--json"-        , "--attr"-        , "config.out.dockerComposeYamlAttrs"+        [ "--eval",+          "--strict",+          "--json",+          "--attr",+          "config.out.dockerComposeYamlAttrs"         ]       args =-        [ evalComposition ]-        ++ commandArgs-        ++ modeArguments (evalMode ea)-        ++ argArgs ea-        ++ map toS (evalUserArgs ea)-      procSpec = (proc "nix-instantiate" args)-        { cwd = evalWorkDir ea-        , std_out = CreatePipe-        }-  +        [evalComposition]+          ++ commandArgs+          ++ modeArguments ea.mode+          ++ argArgs ea+          ++ map toS ea.extraNixArgs+      procSpec =+        (proc "nix-instantiate" args)+          { cwd = ea.workDir,+            std_out = CreatePipe+          }+   withCreateProcess procSpec $ \_in outHM _err procHandle -> do     let outHandle = fromMaybe (panic "stdout missing") outHM @@ -77,7 +76,7 @@      case v of       Right r -> pure r-      Left  e -> throwIO $ FatalError ("Couldn't parse nix-instantiate output" <> show e)+      Left e -> throwIO $ FatalError ("Couldn't parse nix-instantiate output" <> show e)  -- | Run with docker-compose.yaml tmpfile withEvaluatedComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r@@ -88,25 +87,23 @@     hClose yamlHandle     f path - buildComposition :: FilePath -> EvaluationArgs -> IO () buildComposition outLink ea = do   evalComposition <- getEvalCompositionFile   let commandArgs =-        [ "--attr"-        , "config.out.dockerComposeYaml"-        , "--out-link"-        , outLink+        [ "--attr",+          "config.out.dockerComposeYaml",+          "--out-link",+          outLink         ]       args =-        [ evalComposition ]-        ++ commandArgs-        ++ argArgs ea-        ++ map toS (evalUserArgs ea)-      procSpec = (proc "nix-build" args) { cwd = evalWorkDir ea }-  -  withCreateProcess procSpec $ \_in _out _err procHandle -> do+        [evalComposition]+          ++ commandArgs+          ++ argArgs ea+          ++ map toS ea.extraNixArgs+      procSpec = (proc "nix-build" args) {cwd = ea.workDir} +  withCreateProcess procSpec $ \_in _out _err procHandle -> do     exitCode <- waitForProcess procHandle      case exitCode of@@ -126,58 +123,57 @@     buildComposition path ea     f path - replForComposition :: EvaluationArgs -> IO () replForComposition ea = do-    evalComposition <- getEvalCompositionFile-    let args =-          [ "repl", evalComposition ]+  evalComposition <- getEvalCompositionFile+  let args =+        ["repl", "--file", evalComposition]           ++ argArgs ea-          ++ map toS (evalUserArgs ea)-        procSpec = (proc "nix" args) { cwd = evalWorkDir ea }-    -    withCreateProcess procSpec $ \_in _out _err procHandle -> do+          ++ map toS ea.extraNixArgs+      procSpec = (proc "nix" args) {cwd = ea.workDir} -      exitCode <- waitForProcess procHandle+  withCreateProcess procSpec $ \_in _out _err procHandle -> do+    exitCode <- waitForProcess procHandle -      case exitCode of-        ExitSuccess -> pass-        ExitFailure 1 -> exitFailure-        ExitFailure {} -> do-          throwIO $ FatalError $ "nix repl failed with " <> show exitCode+    case exitCode of+      ExitSuccess -> pass+      ExitFailure 1 -> exitFailure+      ExitFailure {} -> do+        throwIO $ FatalError $ "nix repl failed with " <> show exitCode  argArgs :: EvaluationArgs -> [[Char]] argArgs ea =-      [ "--argstr"-      , "uid"-      , show $ evalUid ea-      , "--arg"-      , "modules"-      , modulesNixExpr $ evalModules ea-      , "--arg"-      , "pkgs"-      , toS $ evalPkgs ea-      ]+  [ "--argstr",+    "uid",+    show ea.posixUID,+    "--arg",+    "modules",+    modulesNixExpr ea.evalModulesFile,+    "--arg",+    "pkgs",+    toS ea.pkgsExpr+  ]  getEvalCompositionFile :: IO FilePath getEvalCompositionFile = getDataFileName "nix/eval-composition.nix"  modeArguments :: EvaluationMode -> [[Char]]-modeArguments ReadWrite = [ "--read-write-mode" ]-modeArguments ReadOnly = [ "--readonly-mode" ]+modeArguments ReadWrite = ["--read-write-mode"]+modeArguments ReadOnly = ["--readonly-mode"]  modulesNixExpr :: NonEmpty FilePath -> [Char] modulesNixExpr =   NE.toList >>> fmap pathExpr >>> Data.String.unwords >>> wrapList- where-  pathExpr :: FilePath -> [Char]-  pathExpr path | isAbsolute path = "(/. + \"/${" <> toNixStringLiteral path <> "}\")"-                | otherwise       = "(./. + \"/${" <> toNixStringLiteral path <> "}\")"+  where+    pathExpr :: FilePath -> [Char]+    pathExpr path+      | isAbsolute path = "(/. + \"/${" <> toNixStringLiteral path <> "}\")"+      | otherwise = "(./. + \"/${" <> toNixStringLiteral path <> "}\")" -  isAbsolute ('/' : _) = True-  isAbsolute _         = False+    isAbsolute ('/' : _) = True+    isAbsolute _ = False -  wrapList s = "[ " <> s <> " ]"+    wrapList s = "[ " <> s <> " ]"  toNixStringLiteral :: [Char] -> [Char] toNixStringLiteral = show -- FIXME: custom escaping including '$'
src/haskell/lib/Arion/Services.hs view
@@ -1,20 +1,17 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE CPP #-}-module Arion.Services-  ( getDefaultExec-  ) where -import Prelude()-import Protolude hiding (to)+module Arion.Services+  ( getDefaultExec,+  )+where  import qualified Data.Aeson as Aeson+import Protolude hiding (to)+import Prelude () #if MIN_VERSION_lens_aeson(1,2,0) import qualified Data.Aeson.Key as AK #endif-import           Arion.Aeson (decodeFile)-+import Arion.Aeson (decodeFile) import Control.Lens import Data.Aeson.Lens @@ -31,7 +28,6 @@ -- | Subject to change getDefaultExec :: FilePath -> Text -> IO [Text] getDefaultExec fp service = do-   v <- decodeFile fp    pure ((v :: Aeson.Value) ^.. key "x-arion" . key "serviceInfo" . key (mkKey service) . key "defaultExec" . _Array . traverse . _String)
src/haskell/test/Arion/NixSpec.hs view
@@ -1,52 +1,75 @@-{-# LANGUAGE OverloadedStrings #-} module Arion.NixSpec-  ( spec+  ( spec,   ) where -import           Protolude-import           Test.Hspec-import qualified Data.List.NonEmpty            as NEL-import           Arion.Aeson-import           Arion.Nix-import qualified Data.Text                     as T-import qualified Data.Text.IO                  as T+import Arion.Aeson+import Arion.Nix+import qualified Data.List.NonEmpty as NEL+import qualified Data.Text as T+import qualified Data.Text.IO as T+import Protolude+import Test.Hspec  spec :: Spec-spec = describe "evaluateComposition" $ it "matches an example" $ do-  x <- Arion.Nix.evaluateComposition EvaluationArgs-    { evalUid      = 123-    , evalModules  = NEL.fromList-                       ["src/haskell/testdata/Arion/NixSpec/arion-compose.nix"]-    , evalPkgs     = "import <nixpkgs> { system = \"x86_64-linux\"; }"-    , evalWorkDir  = Nothing-    , evalMode     = ReadOnly-    , evalUserArgs = ["--show-trace"]-    }-  let actual = pretty x-  expected <- T.readFile "src/haskell/testdata/Arion/NixSpec/arion-compose.json"-  censorPaths actual `shouldBe` censorPaths expected+spec = describe "evaluateComposition" $ do+  it "matches an example" $ do+    x <-+      Arion.Nix.evaluateComposition+        EvaluationArgs+          { posixUID = 123,+            evalModulesFile =+              NEL.fromList+                ["src/haskell/testdata/Arion/NixSpec/arion-compose.nix"],+            pkgsExpr = "import <nixpkgs> { system = \"x86_64-linux\"; }",+            workDir = Nothing,+            mode = ReadOnly,+            extraNixArgs = ["--show-trace"]+          }+    let actual = pretty x+    expected <- T.readFile "src/haskell/testdata/Arion/NixSpec/arion-compose.json"+    censorPaths actual `shouldBe` censorPaths expected +  it "matches an build.context example" $ do+    x <-+      Arion.Nix.evaluateComposition+        EvaluationArgs+          { posixUID = 1234,+            evalModulesFile =+              NEL.fromList+                ["src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix"],+            pkgsExpr = "import <nixpkgs> { system = \"x86_64-linux\"; }",+            workDir = Nothing,+            mode = ReadOnly,+            extraNixArgs = ["--show-trace"]+          }+    let actual = pretty x+    expected <- T.readFile "src/haskell/testdata/Arion/NixSpec/arion-context-compose.json"+    censorPaths actual `shouldBe` censorPaths expected+ censorPaths :: Text -> Text censorPaths = censorImages . censorStorePaths  censorStorePaths :: Text -> Text censorStorePaths x = case T.breakOn "/nix/store/" x of   (prefix, tl) | (tl :: Text) == "" -> prefix-  (prefix, tl) -> prefix <> "<STOREPATH>" <> censorPaths-    (T.dropWhile isNixNameChar $ T.drop (T.length "/nix/store/") tl)+  (prefix, tl) ->+    prefix+      <> "<STOREPATH>"+      <> censorPaths+        (T.dropWhile isNixNameChar $ T.drop (T.length "/nix/store/") tl)  -- Probably slow, due to not O(1) <> censorImages :: Text -> Text censorImages x = case T.break (\c -> c == ':' || c == '"') x of   (prefix, tl) | tl == "" -> prefix-  (prefix, tl) | let imageId = T.take 33 (T.drop 1 tl)-               , T.last imageId == '\"'-                 -- Approximation of nix hash validation-               , T.all (\c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) (T.take 32 imageId)-               -> prefix <> T.take 1 tl <> "<HASH>" <> censorImages (T.drop 33 tl)+  (prefix, tl)+    | let imageId = T.take 33 (T.drop 1 tl),+      T.last imageId == '\"',+      -- Approximation of nix hash validation+      T.all (\c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) (T.take 32 imageId) ->+        prefix <> T.take 1 tl <> "<HASH>" <> censorImages (T.drop 33 tl)   (prefix, tl) -> prefix <> T.take 1 tl <> censorImages (T.drop 1 tl)-  -- | WARNING: THIS IS LIKELY WRONG: DON'T REUSE isNixNameChar :: Char -> Bool
src/haskell/test/Spec.hs view
@@ -1,10 +1,10 @@ module Spec-  ( spec+  ( spec,   ) where -import           Test.Hspec import qualified Arion.NixSpec+import Test.Hspec  spec :: Spec spec = do
src/haskell/test/TestMain.hs view
@@ -1,10 +1,11 @@ module Main where -import           Prelude()-import           Protolude-import           Test.Hspec.Runner+import Protolude import qualified Spec+import Test.Hspec.Runner+import Prelude ()  main :: IO () main = hspecWith config Spec.spec-  where config = defaultConfig { configColorMode = ColorAlways }+  where+    config = defaultConfig {configColorMode = ColorAlways}
src/haskell/testdata/Arion/NixSpec/arion-compose.nix view
@@ -2,7 +2,7 @@   project.name = "unit-test-data";   services.webserver = { pkgs, ... }: {     nixos.useSystemd = true;-    nixos.configuration.boot.tmpOnTmpfs = true;+    nixos.configuration.boot.tmp.useTmpfs = true;     nixos.configuration.services.nginx.enable = true;     nixos.configuration.services.nginx.virtualHosts.localhost.root = "${pkgs.nix.doc}/share/doc/nix/manual";     service.useHostStore = true;
+ src/haskell/testdata/Arion/NixSpec/arion-context-compose.json view
@@ -0,0 +1,41 @@+{+    "networks": {+        "default": {+            "name": "unit-test-data"+        }+    },+    "services": {+        "webserver": {+            "build": {+                "context": "<STOREPATH>"+            },+            "environment": {},+            "ports": [+                "8080:80"+            ],+            "sysctls": {},+            "volumes": []+        }+    },+    "version": "3.4",+    "volumes": {},+    "x-arion": {+        "images": [+            {+                "imageExe": "<STOREPATH>",+                "imageName": "localhost/webserver",+                "imageTag": "<HASH>"+            }+        ],+        "project": {+            "name": "unit-test-data"+        },+        "serviceInfo": {+            "webserver": {+                "defaultExec": [+                    "/bin/sh"+                ]+            }+        }+    }+}
+ src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix view
@@ -0,0 +1,9 @@+{+  project.name = "unit-test-data";+  services.webserver.service = {+    build.context = "${./build-context}";+    ports = [+      "8080:80"+    ];+  };+}
src/nix/lib.nix view
@@ -3,11 +3,13 @@    link = url: text: ''[${text}](${url})''; +  composeSpecRev = "55b450aee50799a2f33cc99e1d714518babe305e";+   serviceRef = fragment:-    ''See ${link "https://docs.docker.com/compose/compose-file/05-services/#${fragment}" "Docker Compose Services #${fragment}"}'';+    ''See ${link "https://github.com/compose-spec/compose-spec/blob/${composeSpecRev}/05-services.md#${fragment}" "Compose Spec Services #${fragment}"}'';    networkRef = fragment:-    ''See ${link "https://docs.docker.com/compose/compose-file/06-networks/#${fragment}" "Docker Compose Network #${fragment}"}'';+    ''See ${link "https://github.com/compose-spec/compose-spec/blob/${composeSpecRev}/06-networks.md#${fragment}" "Compose Spec Networks #${fragment}"}'';  in {
src/nix/modules/service/docker-compose-service.nix view
@@ -66,6 +66,22 @@         https://docs.docker.com/compose/compose-file/build/#context       '';     };+    service.build.dockerfile = mkOption {+      type = nullOr str;+      default = null;+      description = ''+        Sets an alternate Dockerfile. A relative path is resolved from the build context.+        https://docs.docker.com/compose/compose-file/build/#dockerfile+      '';+    };+    service.build.target = mkOption {+      type = nullOr str;+      default = null;+      description = ''+        Defines the stage to build as defined inside a multi-stage Dockerfile.+        https://docs.docker.com/compose/compose-file/build/#target+      '';+    };     service.hostname = mkOption {       type = nullOr str;       default = null;@@ -86,7 +102,8 @@       description = serviceRef "environment";     };     service.image = mkOption {-      type = str;+      type = nullOr str;+      default = null;       description = serviceRef "image";     };     service.command = mkOption {@@ -296,6 +313,11 @@       default = null;       description = serviceRef "stop_signal";     };+    service.stop_grace_period = mkOption {+      type = nullOr str;+      default = null;+      description = serviceRef "stop_grace_period";+    };     service.sysctls = mkOption {       type = attrsOf (either str int);       default = {};@@ -321,6 +343,103 @@         ${serviceRef "cap_drop"}       '';     };+    service.blkio_config = mkOption {+      default = null;+      example = { weight = 300; };+      description = ''+        Block IO limits for the service.++        ${serviceRef "blkio_config"}+      '';+      type = nullOr (+        submodule (+          { config, options, ... }:+          let+            deviceBpsModule = submodule (+              { config, options, ... }:+              {+                options = {+                  path = mkOption {+                    type = str;+                    example = "/dev/sda";+                    description = serviceRef "blkio_config";+                  };+                  rate = mkOption {+                    type = str;+                    example = "12mb";+                    description = serviceRef "blkio_config";+                  };+                };+              }+            );+            deviceIopsModule = submodule (+              { config, options, ... }:+              {+                options = {+                  path = mkOption {+                    type = str;+                    example = "/dev/sda";+                    description = serviceRef "blkio_config";+                  };+                  rate = mkOption {+                    type = int;+                    example = 120;+                    description = serviceRef "blkio_config";+                  };+                };+              }+            );+          in+          {+            options = {+              _out = mkOption {+                internal = true;+                readOnly = true;+                default = lib.mapAttrs (k: opt: opt.value) (lib.filterAttrs (_: opt: opt.isDefined) { inherit (options) weight weight_device device_read_bps device_write_bps device_read_iops device_write_iops; });+              };+              weight = mkOption {+                type = int;+                example = 300;+                description = serviceRef "blkio_config";+              };+              weight_device = mkOption {+                description = serviceRef "blkio_config";+                type = listOf (submodule ({ config, options, ... }: {+                  options = {+                    path = mkOption {+                      type = str;+                      example = "/dev/sda";+                      description = serviceRef "blkio_config";+                    };+                    weight = mkOption {+                      type = int;+                      example = 400;+                      description = serviceRef "blkio_config";+                    };+                  };+                }));+              };+              device_read_bps = mkOption {+                type = listOf (deviceBpsModule);+                description = serviceRef "blkio_config";+              };+              device_write_bps = mkOption {+                type = listOf (deviceBpsModule);+                description = serviceRef "blkio_config";+              };+              device_read_iops = mkOption {+                type = listOf (deviceIopsModule);+                description = serviceRef "blkio_config";+              };+              device_write_iops = mkOption {+                type = listOf (deviceIopsModule);+                description = serviceRef "blkio_config";+              };+            };+          }+        )+      );+    };   };    config.out.service = {@@ -328,10 +447,11 @@       volumes       environment       sysctls-      image       ;-  } // lib.optionalAttrs (config.service.build.context != null) {-    inherit (config.service) build;+  } // lib.optionalAttrs (config.service.image != null) {+    inherit (config.service) image;+  } // lib.optionalAttrs (config.service.build.context != null ) {+    build = lib.filterAttrs (n: v: v != null)  config.service.build;   } // lib.optionalAttrs (cap_add != []) {     inherit cap_add;   } // lib.optionalAttrs (cap_drop != []) {@@ -378,6 +498,8 @@     inherit (config.service) restart;   } // lib.optionalAttrs (config.service.stop_signal != null) {     inherit (config.service) stop_signal;+  } // lib.optionalAttrs (config.service.stop_grace_period != null) {+    inherit (config.service) stop_grace_period;   } // lib.optionalAttrs (config.service.tmpfs != []) {     inherit (config.service) tmpfs;   } // lib.optionalAttrs (config.service.tty != null) {@@ -386,5 +508,7 @@     inherit (config.service) working_dir;   } // lib.optionalAttrs (config.service.user != null) {     inherit (config.service) user;+  } // lib.optionalAttrs (config.service.blkio_config != null) {+    blkio_config = config.service.blkio_config._out;   }; }
src/nix/modules/service/image.nix view
@@ -163,17 +163,19 @@       '';     };   };-  config = {-    build.image = builtImage;-    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));--    service.image = lib.mkDefault "${config.build.imageName}:${config.build.imageTag}";-    image.rawConfig.Cmd = config.image.command;--    image.nixBuild = lib.mkDefault (priorityIsDefault options.service.image);-  };+  config = lib.mkMerge [{+      build.image = builtImage;+      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}";+    })+  ]; }
src/nix/modules/service/nixos-init.nix view
@@ -39,7 +39,7 @@     service.tmpfs = [       "/run"          # noexec is fine because exes should be symlinked from elsewhere anyway       "/run/wrappers" # noexec breaks this intentionally-    ] ++ lib.optional (config.nixos.evaluatedConfig.boot.tmpOnTmpfs) "/tmp:exec,mode=777";+    ] ++ lib.optional (config.nixos.evaluatedConfig.boot.tmp.useTmpfs) "/tmp:exec,mode=777";      service.stop_signal = "SIGRTMIN+3";     service.tty = true;