diff --git a/arion-compose.cabal b/arion-compose.cabal index 1637b44..149839e 100644 --- a/arion-compose.cabal +++ b/arion-compose.cabal @@ -27,6 +27,7 @@ data-dir: src common deps build-depends: base ^>=4.12.0.0 , aeson + , aeson-pretty , async , bytestring , process @@ -41,6 +42,7 @@ flag ghci library import: deps exposed-modules: Arion.Nix + Arion.Aeson other-modules: Paths_arion_compose -- other-extensions: hs-source-dirs: src/haskell/lib @@ -52,7 +54,6 @@ executable arion -- other-modules: -- other-extensions: build-depends: optparse-applicative - , aeson-pretty , arion-compose hs-source-dirs: src/haskell/exe default-language: Haskell2010 @@ -64,7 +65,8 @@ test-suite arion-unit-tests ghc-options: -Wno-missing-home-modules type: exitcode-stdio-1.0 main-is: TestMain.hs - -- other-modules: + other-modules: Spec + , Arion.NixSpec -- other-extensions: build-depends: arion-compose , hspec diff --git a/nix/haskell-overlay.nix b/nix/haskell-overlay.nix index 9de9363..74e43c4 100644 --- a/nix/haskell-overlay.nix +++ b/nix/haskell-overlay.nix @@ -1,3 +1,13 @@ -self: super: hself: hsuper: { - arion-compose = hself.callCabal2nix "arion-compose" ./.. {}; +self: super: hself: hsuper: +let + inherit (self.haskell.lib) addBuildTools overrideCabal; +in +{ + arion-compose = overrideCabal (addBuildTools (hself.callCabal2nix "arion-compose" ./.. {}) [self.nix]) (o: o // { + preCheck = '' + export NIX_LOG_DIR=$TMPDIR + export NIX_STATE_DIR=$TMPDIR + export NIX_PATH=nixpkgs=${self.path} + ''; + }); } \ No newline at end of file diff --git a/src/haskell/exe/Main.hs b/src/haskell/exe/Main.hs index a4a0ec6..ca6242b 100644 --- a/src/haskell/exe/Main.hs +++ b/src/haskell/exe/Main.hs @@ -6,6 +6,7 @@ import Protolude hiding (Down) import Arion.Nix +import Arion.Aeson import Options.Applicative import Control.Applicative @@ -26,6 +27,7 @@ data CommonOptions = CommonOptions { files :: NonEmpty FilePath , pkgs :: Text + , nixArgs :: [Text] } deriving (Show) @@ -54,7 +56,15 @@ parseOptions = do <> value "./arion-pkgs.nix" <> help "Use Nix expression EXPR to get the Nixpkgs attrset used for bootstrapping \ \and evaluating the configuration." ) - pure CommonOptions{..} + showTrace <- flag False True (long "show-trace" + <> help "Causes Nix to print out a stack trace in case of Nix expression evaluation errors.") + -- 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")) + pure $ + let nixArgs = userNixArgs <|> "--show-trace" <$ guard showTrace + in CommonOptions{..} + +textArgument = fmap T.pack . strArgument parseCommand :: Parser (CommonOptions -> IO ()) parseCommand = @@ -135,14 +145,16 @@ runEvalAndDC cmd dopts opts = do runDC cmd dopts opts runCat :: CommonOptions -> IO () -runCat (CommonOptions files pkgs) = do +runCat co = do v <- Arion.Nix.evaluate EvaluationArgs { evalUid = 0 -- TODO - , evalModules = files - , evalPkgs = pkgs + , evalModules = files co + , evalPkgs = pkgs co , evalWorkDir = Nothing + , evalMode = ReadWrite + , evalUserArgs = nixArgs co } - T.hPutStrLn stderr (TL.toStrict $ TB.toLazyText $ Data.Aeson.Encode.Pretty.encodePrettyToTextBuilder v) + T.hPutStrLn stderr (pretty v) runRepl :: CommonOptions -> IO () runRepl opts = diff --git a/src/haskell/lib/Arion/Aeson.hs b/src/haskell/lib/Arion/Aeson.hs new file mode 100644 index 0000000..f36a1c8 --- /dev/null +++ b/src/haskell/lib/Arion/Aeson.hs @@ -0,0 +1,20 @@ +module Arion.Aeson where + +import Data.Aeson +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.IO as TL +import qualified Data.Text.Lazy.Builder as TB +import qualified Data.Aeson.Encode.Pretty +import Data.Aeson.Encode.Pretty ( defConfig + , keyOrder + , confCompare + , confTrailingNewline + ) +import Protolude + +pretty :: ToJSON a => a -> Text +pretty = + TL.toStrict + . TB.toLazyText + . Data.Aeson.Encode.Pretty.encodePrettyToTextBuilder' config + where config = defConfig { confCompare = compare, confTrailingNewline = True } diff --git a/src/haskell/lib/Arion/Nix.hs b/src/haskell/lib/Arion/Nix.hs index f985a90..d955484 100644 --- a/src/haskell/lib/Arion/Nix.hs +++ b/src/haskell/lib/Arion/Nix.hs @@ -21,24 +21,30 @@ import Data.List.NonEmpty ( NonEmpty(..) ) import Control.Arrow ( (>>>) ) +data EvaluationMode = + ReadWrite | ReadOnly + data EvaluationArgs = EvaluationArgs { evalUid :: Int , evalModules :: NonEmpty FilePath , evalPkgs :: Text , evalWorkDir :: Maybe FilePath + , evalMode :: EvaluationMode + , evalUserArgs :: [Text] } evaluate :: EvaluationArgs -> IO Value evaluate ea = do evalComposition <- getDataFileName "nix/eval-composition.nix" - let args = - [ evalComposition - , "--eval" + let commandArgs = + [ "--eval" , "--strict" - , "--read-write-mode" , "--json" - , "--show-trace" - , "--argstr" + , "--attr" + , "config.build.dockerComposeYamlAttrs" + ] + argArgs = + [ "--argstr" , "uid" , show $ evalUid ea , "--arg" @@ -47,9 +53,13 @@ evaluate ea = do , "--arg" , "pkgs" , toS $ evalPkgs ea - , "--attr" - , "config.build.dockerComposeYamlAttrs" ] + args = + [ evalComposition ] + ++ commandArgs + ++ modeArguments (evalMode ea) + ++ argArgs + ++ map toS (evalUserArgs ea) stdin = mempty procSpec = (proc "nix-instantiate" args) { cwd = evalWorkDir ea } @@ -73,6 +83,9 @@ evaluate ea = do Right r -> pure r Left e -> throwIO $ FatalError "Couldn't parse nix-instantiate output" +modeArguments :: EvaluationMode -> [[Char]] +modeArguments ReadWrite = [ "--read-write-mode" ] +modeArguments ReadOnly = [ "--readonly-mode" ] modulesNixExpr :: NonEmpty FilePath -> [Char] modulesNixExpr = diff --git a/src/haskell/test/Arion/FooSpec.hs b/src/haskell/test/Arion/FooSpec.hs deleted file mode 100644 index d4ea5fd..0000000 --- a/src/haskell/test/Arion/FooSpec.hs +++ /dev/null @@ -1,11 +0,0 @@ -module Arion.FooSpec - ( spec - ) -where - -import Test.Hspec -import Test.QuickCheck - -spec :: Spec -spec = do - it "foo" $ property True diff --git a/src/haskell/test/Arion/NixSpec.hs b/src/haskell/test/Arion/NixSpec.hs new file mode 100644 index 0000000..6b2308e --- /dev/null +++ b/src/haskell/test/Arion/NixSpec.hs @@ -0,0 +1,49 @@ +{-# LANGUAGE OverloadedStrings #-} +module Arion.NixSpec + ( spec + ) +where + +import Protolude +import Test.Hspec +import Test.QuickCheck +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 qualified Data.Text.Lazy.IO as TL +import qualified Data.Text.Lazy.Builder as TB +import qualified Data.Aeson.Encode.Pretty +import Data.Char (isSpace) + +spec :: Spec +spec = describe "evaluate" $ it "matches an example" $ do + x <- Arion.Nix.evaluate EvaluationArgs + { evalUid = 123 + , evalModules = NEL.fromList + ["src/haskell/testdata/Arion/NixSpec/arion-compose.nix"] + , evalPkgs = "import {}" + , 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 + +censorPaths :: Text -> Text +censorPaths x = case T.breakOn "/nix/store/" x of + (prefix, tl) | (tl :: Text) == "" -> prefix + (prefix, tl) -> prefix <> "" <> censorPaths + (T.dropWhile isNixNameChar $ T.drop (T.length "/nix/store/") tl) + +-- | WARNING: THIS IS LIKELY WRONG: DON'T REUSE +isNixNameChar :: Char -> Bool +isNixNameChar c | c >= '0' && c <= '9' = True +isNixNameChar c | c >= 'a' && c <= 'z' = True +isNixNameChar c | c >= 'A' && c <= 'Z' = True +isNixNameChar c | c == '-' = True +isNixNameChar c | c == '.' = True +isNixNameChar c | c == '_' = True -- WRONG? +isNixNameChar c = False -- WRONG? diff --git a/src/haskell/test/Spec.hs b/src/haskell/test/Spec.hs index 73ab5ab..d2da234 100644 --- a/src/haskell/test/Spec.hs +++ b/src/haskell/test/Spec.hs @@ -4,8 +4,8 @@ module Spec where import Test.Hspec -import qualified Arion.FooSpec +import qualified Arion.NixSpec spec :: Spec spec = do - describe "Arion.Foo" Arion.FooSpec.spec + describe "Arion.Nix" Arion.NixSpec.spec diff --git a/src/haskell/testdata/Arion/NixSpec/arion-compose.json b/src/haskell/testdata/Arion/NixSpec/arion-compose.json new file mode 100644 index 0000000..198ef05 --- /dev/null +++ b/src/haskell/testdata/Arion/NixSpec/arion-compose.json @@ -0,0 +1,45 @@ +{ + "services": { + "webserver": { + "build": { + "context": "/nix/store/l6jwin74n93d66ralxzb001c22yjii9x-arion-image" + }, + "command": [ + "/nix/store/b9w61w4g8sqgrm3rid6ca22krslqghb3-nixos-system-unnamed-19.03.173100.e726e8291b2/init" + ], + "environment": { + "NIX_REMOTE": "", + "container": "docker" + }, + "image": "arion-base", + "ports": [ + "8000:80" + ], + "stop_signal": "SIGRTMIN+3", + "sysctls": {}, + "tmpfs": [ + "/run", + "/run/wrappers", + "/tmp:exec,mode=777" + ], + "tty": true, + "volumes": [ + "/sys/fs/cgroup:/sys/fs/cgroup:ro", + "/nix/store:/nix/store", + "/nix/store/pssdmhzjnhflawv7rwk1yw39350iv40g-container-system-env:/run/system" + ] + } + }, + "version": "3.4", + "x-arion": { + "images": [], + "serviceInfo": { + "webserver": { + "defaultExec": [ + "/run/current-system/sw/bin/bash", + "-l" + ] + } + } + } +} diff --git a/src/haskell/testdata/Arion/NixSpec/arion-compose.nix b/src/haskell/testdata/Arion/NixSpec/arion-compose.nix new file mode 100644 index 0000000..2ed625c --- /dev/null +++ b/src/haskell/testdata/Arion/NixSpec/arion-compose.nix @@ -0,0 +1,12 @@ +{ + docker-compose.services.webserver = { pkgs, ... }: { + nixos.useSystemd = true; + nixos.configuration.boot.tmpOnTmpfs = true; + nixos.configuration.services.nginx.enable = true; + nixos.configuration.services.nginx.virtualHosts.localhost.root = "${pkgs.nix.doc}/share/doc/nix/manual"; + service.useHostStore = true; + service.ports = [ + "8000:80" # host:container + ]; + }; +}