diff --git a/arion-compose.cabal b/arion-compose.cabal index ac1c333..2920bc6 100644 --- a/arion-compose.cabal +++ b/arion-compose.cabal @@ -49,6 +49,7 @@ library exposed-modules: Arion.Nix Arion.Aeson Arion.DockerCompose + Arion.ExtendedInfo Arion.Images Arion.Services other-modules: Paths_arion_compose diff --git a/docs/modules/ROOT/partials/NixOSOptions.adoc b/docs/modules/ROOT/partials/NixOSOptions.adoc index 93448fc..34f4bfb 100644 --- a/docs/modules/ROOT/partials/NixOSOptions.adoc +++ b/docs/modules/ROOT/partials/NixOSOptions.adoc @@ -106,6 +106,26 @@ No Default:: {blank} Read Only:: {blank} No Example:: {blank} +== project.name + +Name of the project. + +See link:https://docs.docker.com/compose/reference/envvars/#compose_project_name[COMPOSE_PROJECT_NAME] + + +[discrete] +=== details + +Type:: null or string +Default:: ++ +---- +null +---- + + +No Example:: {blank} + == services An attribute set of service configurations. A service specifies how to run an image as a container. diff --git a/examples/minimal/arion-compose.nix b/examples/minimal/arion-compose.nix index ab3c7e8..9531a9f 100644 --- a/examples/minimal/arion-compose.nix +++ b/examples/minimal/arion-compose.nix @@ -1,5 +1,6 @@ { pkgs, ... }: { + config.project.name = "webapp"; config.services = { webserver = { diff --git a/src/haskell/exe/Main.hs b/src/haskell/exe/Main.hs index 1816cd4..eeceda1 100644 --- a/src/haskell/exe/Main.hs +++ b/src/haskell/exe/Main.hs @@ -10,6 +10,7 @@ import Arion.Aeson import Arion.Images (loadImages) import qualified Arion.DockerCompose as DockerCompose import Arion.Services (getDefaultExec) +import Arion.ExtendedInfo (loadExtendedInfoFromPath, ExtendedInfo(images, projectName)) import Options.Applicative import Control.Monad.Fail @@ -27,6 +28,9 @@ data CommonOptions = , pkgs :: Text , nixArgs :: [Text] , prebuiltComposeFile :: Maybe FilePath + , noAnsi :: Bool + , compatibility :: Bool + , logLevel :: Maybe Text } deriving (Show) @@ -63,6 +67,11 @@ parseOptions = do ( 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{..} @@ -142,20 +151,38 @@ runDC cmd (DockerComposeArgs args) _opts = do runBuildAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () runBuildAndDC cmd dopts opts = do - withBuiltComposeFile opts $ \path -> do - loadImages path - DockerCompose.run DockerCompose.Args - { files = [path] - , otherArgs = [cmd] ++ unDockerComposeArgs dopts - } + withBuiltComposeFile opts $ callDC cmd dopts opts True runEvalAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () runEvalAndDC cmd dopts opts = do - withComposeFile opts $ \path -> - DockerCompose.run DockerCompose.Args - { files = [path] - , otherArgs = [cmd] ++ unDockerComposeArgs dopts - } + withComposeFile opts $ callDC cmd dopts opts False + +callDC :: Text -> DockerComposeArgs -> CommonOptions -> Bool -> FilePath -> IO () +callDC cmd dopts opts shouldLoadImages path = do + extendedInfo <- loadExtendedInfoFromPath path + when shouldLoadImages $ loadImages (images extendedInfo) + let firstOpts = projectArgs extendedInfo <> commonArgs opts + DockerCompose.run DockerCompose.Args + { files = [path] + , otherArgs = firstOpts ++ [cmd] ++ unDockerComposeArgs dopts + } + +projectArgs :: ExtendedInfo -> [Text] +projectArgs extendedInfo = + do + n <- toList (projectName extendedInfo) + ["--project-name", n] + +commonArgs :: CommonOptions -> [Text] +commonArgs opts = do + guard (noAnsi opts) + ["--no-ansi"] + <> do + guard (compatibility opts) + ["--compatibility"] + <> do + l <- toList (logLevel opts) + ["--log-level", l] withBuiltComposeFile :: CommonOptions -> (FilePath -> IO r) -> IO r withBuiltComposeFile opts cont = case prebuiltComposeFile opts of @@ -255,12 +282,18 @@ orEmpty' :: (Alternative f, Monoid a) => f a -> f a orEmpty' m = fromMaybe mempty <$> optional m runExec :: Bool -> Bool -> Maybe Text -> Bool -> Int -> [(Text, Text)] -> Maybe Text -> Text -> [Text] -> CommonOptions -> IO () -runExec detach privileged user noTTY index envs workDir service commandAndArgs opts = do - putErrText $ "Service: " <> service - +runExec detach privileged user noTTY index envs workDir service commandAndArgs opts = withComposeFile opts $ \path -> do + extendedInfo <- loadExtendedInfoFromPath path commandAndArgs'' <- case commandAndArgs of - [] -> getDefaultExec path service + [] -> do + cmd <- getDefaultExec path service + case cmd of + [] -> do + putErrText "You must provide a command via service.defaultExec or on the command line." + exitFailure + _ -> + pure cmd x -> pure x let commandAndArgs' = case commandAndArgs'' of [] -> ["/bin/sh"] @@ -280,7 +313,7 @@ runExec detach privileged user noTTY index envs workDir service commandAndArgs o ] DockerCompose.run DockerCompose.Args { files = [path] - , otherArgs = args + , otherArgs = projectArgs extendedInfo <> commonArgs opts <> args } main :: IO () diff --git a/src/haskell/lib/Arion/ExtendedInfo.hs b/src/haskell/lib/Arion/ExtendedInfo.hs new file mode 100644 index 0000000..92a7668 --- /dev/null +++ b/src/haskell/lib/Arion/ExtendedInfo.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{- + +Parses the x-arion field in the generated compose file. + +-} +module Arion.ExtendedInfo where + +import Prelude() +import Protolude +import Data.Aeson as Aeson +import Arion.Aeson +import Control.Lens +import Data.Aeson.Lens + +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) + +data ExtendedInfo = ExtendedInfo { + projectName :: Maybe Text, + images :: [Image] + } deriving (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 + } diff --git a/src/haskell/lib/Arion/Images.hs b/src/haskell/lib/Arion/Images.hs index 40e0108..350ac93 100644 --- a/src/haskell/lib/Arion/Images.hs +++ b/src/haskell/lib/Arion/Images.hs @@ -8,38 +8,23 @@ module Arion.Images import Prelude() import Protolude hiding (to) -import qualified Data.Aeson as Aeson -import Arion.Aeson (decodeFile) import qualified System.Process as Process import qualified Data.Text as T -import Control.Lens -import Data.Aeson.Lens - -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 (Generic, Aeson.ToJSON, Aeson.FromJSON, Show) +import Arion.ExtendedInfo (Image(..)) type TaggedImage = Text -- | Subject to change -loadImages :: FilePath -> IO () -loadImages fp = do - - v <- decodeFile fp +loadImages :: [Image] -> IO () +loadImages requestedImages = do loaded <- getDockerImages let - images :: [Image] - images = (v :: Aeson.Value) ^.. key "x-arion" . key "images" . _Array . traverse . _JSON - isNew i = (imageName i <> ":" <> imageTag i) `notElem` loaded - traverse_ loadImage . filter isNew $ images + traverse_ loadImage . filter isNew $ requestedImages loadImage :: Image -> IO () loadImage (Image { image = Just imgPath, imageName = name }) = diff --git a/src/haskell/testdata/Arion/NixSpec/arion-compose.json b/src/haskell/testdata/Arion/NixSpec/arion-compose.json index 4b389e1..479fe96 100644 --- a/src/haskell/testdata/Arion/NixSpec/arion-compose.json +++ b/src/haskell/testdata/Arion/NixSpec/arion-compose.json @@ -37,6 +37,9 @@ "imageTag": "" } ], + "project": { + "name": null + }, "serviceInfo": { "webserver": { "defaultExec": [ diff --git a/src/haskell/testdata/docker-compose-example.json b/src/haskell/testdata/docker-compose-example.json index abcdd61..6fdaa0b 100644 --- a/src/haskell/testdata/docker-compose-example.json +++ b/src/haskell/testdata/docker-compose-example.json @@ -30,6 +30,9 @@ "imageTag": "xr4ljmz3qfcwlq9rl4mr4qdrzw93rl70" } ], + "project": { + "name": null + }, "serviceInfo": { "webserver": { "defaultExec": [ diff --git a/src/nix/modules.nix b/src/nix/modules.nix index 64f3650..b176b2e 100644 --- a/src/nix/modules.nix +++ b/src/nix/modules.nix @@ -4,4 +4,5 @@ ./modules/composition/images.nix ./modules/composition/service-info.nix ./modules/composition/arion-base-image.nix + ./modules/composition/composition.nix ] \ No newline at end of file diff --git a/src/nix/modules/composition/composition.nix b/src/nix/modules/composition/composition.nix new file mode 100644 index 0000000..fa46617 --- /dev/null +++ b/src/nix/modules/composition/composition.nix @@ -0,0 +1,24 @@ +{ config, lib, ... }: +let + inherit (lib) types mkOption; + + link = url: text: + ''link:${url}[${text}]''; + +in +{ + options = { + project.name = mkOption { + description = '' + Name of the project. + + See ${link "https://docs.docker.com/compose/reference/envvars/#compose_project_name" "COMPOSE_PROJECT_NAME"} + ''; + type = types.nullOr types.str; + default = null; + }; + }; + config = { + docker-compose.extended.project.name = config.project.name; + }; +}