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 +23/−0
- Setup.hs +1/−0
- arion-compose.cabal +10/−1
- src/haskell/exe/Main.hs +199/−174
- src/haskell/lib/Arion/Aeson.hs +15/−13
- src/haskell/lib/Arion/DockerCompose.hs +7/−9
- src/haskell/lib/Arion/ExtendedInfo.hs +22/−19
- src/haskell/lib/Arion/Images.hs +29/−36
- src/haskell/lib/Arion/Nix.hs +91/−95
- src/haskell/lib/Arion/Services.hs +7/−11
- src/haskell/test/Arion/NixSpec.hs +53/−30
- src/haskell/test/Spec.hs +2/−2
- src/haskell/test/TestMain.hs +5/−4
- src/haskell/testdata/Arion/NixSpec/arion-compose.nix +1/−1
- src/haskell/testdata/Arion/NixSpec/arion-context-compose.json +41/−0
- src/haskell/testdata/Arion/NixSpec/arion-context-compose.nix +9/−0
- src/nix/lib.nix +4/−2
- src/nix/modules/service/docker-compose-service.nix +128/−4
- src/nix/modules/service/image.nix +15/−13
- src/nix/modules/service/nixos-init.nix +1/−1
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;