From df0ec2eb5050e9c1e9cd76f4d78f74ee400f2d72 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 10 Oct 2020 22:04:52 +0200 Subject: [PATCH 1/5] Add name option for project/composition name --- arion-compose.cabal | 1 + examples/minimal/arion-compose.nix | 1 + src/haskell/exe/Main.hs | 27 +++++++++------ src/haskell/lib/Arion/ExtendedInfo.hs | 37 +++++++++++++++++++++ src/haskell/lib/Arion/Images.hs | 23 +++---------- src/nix/modules.nix | 1 + src/nix/modules/composition/composition.nix | 24 +++++++++++++ 7 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 src/haskell/lib/Arion/ExtendedInfo.hs create mode 100644 src/nix/modules/composition/composition.nix 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/examples/minimal/arion-compose.nix b/examples/minimal/arion-compose.nix index ab3c7e8..e864794 100644 --- a/examples/minimal/arion-compose.nix +++ b/examples/minimal/arion-compose.nix @@ -1,5 +1,6 @@ { pkgs, ... }: { + config.name = "webapp"; config.services = { webserver = { diff --git a/src/haskell/exe/Main.hs b/src/haskell/exe/Main.hs index 1816cd4..ca219c9 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, name)) import Options.Applicative import Control.Monad.Fail @@ -142,20 +143,24 @@ 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 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 False + +callDC :: Text -> DockerComposeArgs -> Bool -> FilePath -> IO () +callDC cmd dopts shouldLoadImages path = do + extendedInfo <- loadExtendedInfoFromPath path + when shouldLoadImages $ loadImages (images extendedInfo) + let firstOpts = + do + n <- toList (name extendedInfo) + ["--project-name", n] + DockerCompose.run DockerCompose.Args + { files = [path] + , otherArgs = firstOpts ++ [cmd] ++ unDockerComposeArgs dopts + } withBuiltComposeFile :: CommonOptions -> (FilePath -> IO r) -> IO r withBuiltComposeFile opts cont = case prebuiltComposeFile opts of diff --git a/src/haskell/lib/Arion/ExtendedInfo.hs b/src/haskell/lib/Arion/ExtendedInfo.hs new file mode 100644 index 0000000..dbdf338 --- /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 { + name :: Maybe Text, + images :: [Image] + } deriving (Eq, Show) + +loadExtendedInfoFromPath :: FilePath -> IO ExtendedInfo +loadExtendedInfoFromPath fp = do + v <- decodeFile fp + pure ExtendedInfo { + -- TODO: use aeson derived instance? + name = v ^? key "x-arion" . 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/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..fe4daf9 --- /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 = { + 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.name = config.name; + }; +} From 2de4188b9de9a16e109c372c85b1200e1bef92a8 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 10 Oct 2020 22:12:37 +0200 Subject: [PATCH 2/5] Update options --- docs/modules/ROOT/partials/NixOSOptions.adoc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/modules/ROOT/partials/NixOSOptions.adoc b/docs/modules/ROOT/partials/NixOSOptions.adoc index 93448fc..98253bc 100644 --- a/docs/modules/ROOT/partials/NixOSOptions.adoc +++ b/docs/modules/ROOT/partials/NixOSOptions.adoc @@ -68,6 +68,26 @@ intended for production-like deployment scenarios. Type:: signed integer No Default:: {blank} +No Example:: {blank} + +== 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} == out.dockerComposeYaml From 92c389fab5e23f4fb7011829d8c5faef60a17725 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sat, 10 Oct 2020 22:14:22 +0200 Subject: [PATCH 3/5] Fix tests --- src/haskell/testdata/Arion/NixSpec/arion-compose.json | 1 + src/haskell/testdata/docker-compose-example.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/haskell/testdata/Arion/NixSpec/arion-compose.json b/src/haskell/testdata/Arion/NixSpec/arion-compose.json index 4b389e1..63c239d 100644 --- a/src/haskell/testdata/Arion/NixSpec/arion-compose.json +++ b/src/haskell/testdata/Arion/NixSpec/arion-compose.json @@ -37,6 +37,7 @@ "imageTag": "" } ], + "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..6392c4d 100644 --- a/src/haskell/testdata/docker-compose-example.json +++ b/src/haskell/testdata/docker-compose-example.json @@ -30,6 +30,7 @@ "imageTag": "xr4ljmz3qfcwlq9rl4mr4qdrzw93rl70" } ], + "name": null, "serviceInfo": { "webserver": { "defaultExec": [ From b959ab492d0a435ef3aab9df9617d1c303f195f6 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 11 Oct 2020 12:02:58 +0200 Subject: [PATCH 4/5] Move name -> project.name --- docs/modules/ROOT/partials/NixOSOptions.adoc | 40 +++++++++---------- examples/minimal/arion-compose.nix | 2 +- src/haskell/exe/Main.hs | 4 +- src/haskell/lib/Arion/ExtendedInfo.hs | 4 +- .../testdata/Arion/NixSpec/arion-compose.json | 4 +- .../testdata/docker-compose-example.json | 4 +- src/nix/modules/composition/composition.nix | 4 +- 7 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/modules/ROOT/partials/NixOSOptions.adoc b/docs/modules/ROOT/partials/NixOSOptions.adoc index 98253bc..34f4bfb 100644 --- a/docs/modules/ROOT/partials/NixOSOptions.adoc +++ b/docs/modules/ROOT/partials/NixOSOptions.adoc @@ -68,26 +68,6 @@ intended for production-like deployment scenarios. Type:: signed integer No Default:: {blank} -No Example:: {blank} - -== 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} == out.dockerComposeYaml @@ -126,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 e864794..9531a9f 100644 --- a/examples/minimal/arion-compose.nix +++ b/examples/minimal/arion-compose.nix @@ -1,6 +1,6 @@ { pkgs, ... }: { - config.name = "webapp"; + config.project.name = "webapp"; config.services = { webserver = { diff --git a/src/haskell/exe/Main.hs b/src/haskell/exe/Main.hs index ca219c9..0492b2c 100644 --- a/src/haskell/exe/Main.hs +++ b/src/haskell/exe/Main.hs @@ -10,7 +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, name)) +import Arion.ExtendedInfo (loadExtendedInfoFromPath, ExtendedInfo(images, projectName)) import Options.Applicative import Control.Monad.Fail @@ -155,7 +155,7 @@ callDC cmd dopts shouldLoadImages path = do when shouldLoadImages $ loadImages (images extendedInfo) let firstOpts = do - n <- toList (name extendedInfo) + n <- toList (projectName extendedInfo) ["--project-name", n] DockerCompose.run DockerCompose.Args { files = [path] diff --git a/src/haskell/lib/Arion/ExtendedInfo.hs b/src/haskell/lib/Arion/ExtendedInfo.hs index dbdf338..92a7668 100644 --- a/src/haskell/lib/Arion/ExtendedInfo.hs +++ b/src/haskell/lib/Arion/ExtendedInfo.hs @@ -23,7 +23,7 @@ data Image = Image } deriving (Eq, Show, Generic, Aeson.ToJSON, Aeson.FromJSON) data ExtendedInfo = ExtendedInfo { - name :: Maybe Text, + projectName :: Maybe Text, images :: [Image] } deriving (Eq, Show) @@ -32,6 +32,6 @@ loadExtendedInfoFromPath fp = do v <- decodeFile fp pure ExtendedInfo { -- TODO: use aeson derived instance? - name = v ^? key "x-arion" . key "name" . _String, + 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/testdata/Arion/NixSpec/arion-compose.json b/src/haskell/testdata/Arion/NixSpec/arion-compose.json index 63c239d..479fe96 100644 --- a/src/haskell/testdata/Arion/NixSpec/arion-compose.json +++ b/src/haskell/testdata/Arion/NixSpec/arion-compose.json @@ -37,7 +37,9 @@ "imageTag": "" } ], - "name": null, + "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 6392c4d..6fdaa0b 100644 --- a/src/haskell/testdata/docker-compose-example.json +++ b/src/haskell/testdata/docker-compose-example.json @@ -30,7 +30,9 @@ "imageTag": "xr4ljmz3qfcwlq9rl4mr4qdrzw93rl70" } ], - "name": null, + "project": { + "name": null + }, "serviceInfo": { "webserver": { "defaultExec": [ diff --git a/src/nix/modules/composition/composition.nix b/src/nix/modules/composition/composition.nix index fe4daf9..fa46617 100644 --- a/src/nix/modules/composition/composition.nix +++ b/src/nix/modules/composition/composition.nix @@ -8,7 +8,7 @@ let in { options = { - name = mkOption { + project.name = mkOption { description = '' Name of the project. @@ -19,6 +19,6 @@ in }; }; config = { - docker-compose.extended.name = config.name; + docker-compose.extended.project.name = config.project.name; }; } From 700297748de4ad2c787f2a1ec8af0893e23458a2 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 11 Oct 2020 12:47:28 +0200 Subject: [PATCH 5/5] Support --no-ansi, --compatibility, --log-level options --- src/haskell/exe/Main.hs | 54 +++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/src/haskell/exe/Main.hs b/src/haskell/exe/Main.hs index 0492b2c..eeceda1 100644 --- a/src/haskell/exe/Main.hs +++ b/src/haskell/exe/Main.hs @@ -28,6 +28,9 @@ data CommonOptions = , pkgs :: Text , nixArgs :: [Text] , prebuiltComposeFile :: Maybe FilePath + , noAnsi :: Bool + , compatibility :: Bool + , logLevel :: Maybe Text } deriving (Show) @@ -64,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{..} @@ -143,25 +151,39 @@ runDC cmd (DockerComposeArgs args) _opts = do runBuildAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () runBuildAndDC cmd dopts opts = do - withBuiltComposeFile opts $ callDC cmd dopts True + withBuiltComposeFile opts $ callDC cmd dopts opts True runEvalAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO () runEvalAndDC cmd dopts opts = do - withComposeFile opts $ callDC cmd dopts False + withComposeFile opts $ callDC cmd dopts opts False -callDC :: Text -> DockerComposeArgs -> Bool -> FilePath -> IO () -callDC cmd dopts shouldLoadImages path = do +callDC :: Text -> DockerComposeArgs -> CommonOptions -> Bool -> FilePath -> IO () +callDC cmd dopts opts shouldLoadImages path = do extendedInfo <- loadExtendedInfoFromPath path when shouldLoadImages $ loadImages (images extendedInfo) - let firstOpts = - do - n <- toList (projectName extendedInfo) - ["--project-name", n] + 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 Just prebuilt -> do @@ -260,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"] @@ -285,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 ()