packages feed

arion-compose (empty) → 0.1.0.0

raw patch · 36 files changed

+2301/−0 lines, 36 filesdep +QuickCheckdep +aesondep +aeson-prettysetup-changed

Dependencies added: QuickCheck, aeson, aeson-pretty, arion-compose, async, base, bytestring, directory, hspec, lens, lens-aeson, optparse-applicative, process, protolude, temporary, text, unix

Files

+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# Revision history for Arion++## Unreleased++<!-- TODO: use better template -->++## 0.1.0.0 -- 2019-10-04++* First released version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,202 @@++                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright 2019 Hercules Labs OÜ++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ README.asciidoc view
@@ -0,0 +1,274 @@+== Introduction++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.++It is built on top of https://docs.docker.com/compose/overview/[Docker+Compose], which implements the container orchestration functionality.++Instead of configuring the compositions in YAML files like+`docker-compose.yaml`, Arion uses the https://nixos.org/nix/[Nix]+language to declare the compositions. Because of this, Arion gives you+the ability to declare your deployments, configuration and packaging+in the same language. By replacing multiple tools with a single+language, you decrease your mental load and you can more easily+refactor and maintain your configurations.++Although Arion can be used as a Docker Compose with an improved+configuration front end, there is more to be gained from integrating+with Nix. In particular, the more structured approach of Nix compared+to Dockerfiles allows the following:++ * Build components of your image in *parallel, automatically*+ * *Share packages between images*, regardless of the order they were+   added+ * Improve performance by *skipping container+   image creation*+ * Work with *structured data instead of strings*,+   templates and a multitude of expression languages+ * Refactor across deployments, configuration and packaging++Arion allows to compose containers with different granularity:++  * <<Minimal: Plain command using nixpkgs>>+  * <<NixOS: run only one systemd service>>+  * <<NixOS: run full OS>>+  * <<Docker image from DockerHub>>++== Installation++=== Nix++```bash+$ nix-env -iA arion -f https://github.com/hercules-ci/arion/tarball/master+```++=== NixOS++Add this module to your NixOS configuration:++```nix+{ ... }: {+  environment.systemPackages = [ (import (builtins.fetchTarball https://github.com/hercules-ci/arion/tarball/master) {}).arion ];+  virtualisation.docker.enable = true;+  users.extraUsers.myuser.extraGroups = ["docker"];+}+```++////++== Not installing: use it in a project++TODO: describe: using nix-shell or in a script, building images as+      part of nix-build, pinning, see also todomvc-nix.++TODO: exposed Nix functions: arion.build, arion.eval (a bit of IFD)+++////+++== Usage++Arion is configured declaratively with two files:++=== arion-pkgs.nix++Arion needs `arion-pkgs.nix` to import nixpkgs, it's contents can be as simple as:++```nix+import <nixpkgs> {}+```++or more sophisticated (recommended) setup with https://github.com/nmattia/niv[Niv].++=== arion-compose.nix++Describe containers using NixOS-style modules. There are a few options:++==== Minimal: Plain command using nixpkgs++`examples/minimal/arion-compose.nix`:++```nix+{ pkgs, ... }:+{+  config.docker-compose.services = {++    webserver = {+      service.useHostStore = true;+      service.command = [ "sh" "-c" ''+                  cd "$$WEB_ROOT"+                  ${pkgs.python3}/bin/python -m http.server+                '' ];+      service.ports = [+        "8000:8000" # host:container+      ];+      service.environment.WEB_ROOT = "${pkgs.nix.doc}/share/doc/nix/manual";+    };+  };+}+++```++==== NixOS: run only one systemd service++`examples/nixos-unit/arion-compose.nix`:++```nix++{+  docker-compose.services.webserver = { config, pkgs, ... }: {++    nixos.configuration = {config, pkgs, ...}: {+      boot.isContainer = true;+      services.nginx.enable = true;+      services.nginx.virtualHosts.localhost.root = "${pkgs.nix.doc}/share/doc/nix/manual";+      system.build.run-nginx = pkgs.writeScript "run-nginx" ''+        #!${pkgs.bash}/bin/bash+        PATH='${config.systemd.services.nginx.environment.PATH}'+        echo nginx:x:${toString config.users.users.nginx.uid}:${toString config.users.groups.nginx.gid}:nginx web server user:/var/empty:/bin/sh >>/etc/passwd+        echo nginx:x:${toString config.users.groups.nginx.gid}:nginx >>/etc/group+        ${config.systemd.services.nginx.runner}+      '';+    };+    service.command = [ config.nixos.build.run-nginx ];+    service.useHostStore = true;+    service.ports = [+      "8000:80" # host:container+    ];+  };+}++```++==== NixOS: run full OS++`examples/full-nixos/arion-compose.nix`:++```nix+{+  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+    ];+  };+}+```++==== Docker image from DockerHub++```nix+{+  docker-compose.services.postgres = {+    service.image = "postgres:10";+    service.volumes = [ "${toString ./.}/postgres-data:/var/lib/postgresql/data" ];+    service.environment.POSTGRES_PASSWORD = "mydefaultpass";+  };+}+```++=== Run++Start containers and watch their logs:++```bash+$ arion up -d+$ arion logs -f+```++You can go to `examples/*/` and run these commands to give it a quick try.++== A full featured Nix command example++To see how Arion can be used in a project, have a look at+https://github.com/nix-community/todomvc-nix/tree/master/deploy/arion[todomvc-nix].++```bash+$ git clone https://github.com/nix-community/todomvc-nix+$ cd todomvc-nix/deploy/arion+$ arion up+```++== Project Status++This project was born out of a process supervision need for local+development environments while working on+https://www.hercules-ci.com[Hercules CI]. (It was also born out of+ancient Greek deities disguised as horses. More on that later.)++If you do want to use Arion for production environments, you’ll probably+want to either build normal container images or manage garbage+collection roots if you control the deployment host. Neither scenario is+made easier by arion at this time.++Arion has run successfully on Linux distributions other than NixOS, but we only perform CI for Arion on NixOS.+++== How it works++Arion is essentially a thin wrapper around Nix and docker-compose. When+it runs, it does the following:++* Evaluate the configuration using Nix, producing a+`docker-compose.yaml` and a garbage collection root+* Invoke `docker-compose`+* Clean up the garbage collection root++Most of the interesting stuff happens in Arion’s Nix expressions, where+it runs the module system (known from NixOS) and provides the+configuration that makes the Docker Compose file do the things it needs+to do.++One of the more interesting built-in modules is the+link:src/nix/modules/service/host-store.nix[host-store.nix module] which+performs the bind mounts to make the host Nix store available in the+container.++== FAQ++=== Do I need to use Hercules CI?++Nope, it’s just Nix and Docker Compose under the hood.++=== What about garbage collection?++Arion removes the need for garbage collecting docker images, delegating+this task to Nix.++Arion creates a garbage collection root and cleans it up after+completing the command. This means that `arion up` without `-d` is safe+with respect to garbage collection. A deployment that is more serious+than local development must leave a GC root on the deployment host. This+use case is not supported as of now.++=== Why is my container not running latest code?++Restart it with `arion restart <name>` or if you've changed the image rebuild+them using `arion up -d --always-recreate-deps <name>`.++=== What is messing with my environment variables?++Docker Compose performs its own environment variable substitution. This+can be a little annoying in `services.command` for example. Either+reference a script from `pkgs.writeScript` or escape the dollar sign as+`$$`.++=== Why name it ``Arion``?++Arion comes from Greek mythology. Poseidon, the god of ~Docker~ the seas+had his eye on Demeter. Demeter tried to trick him by disguising as a+horse, but Poseidon saw through the deception and they had Arion.++So Arion is a super fast divine horse; the result of some weird mixing.+Also it talks.++(And we feel morally obliged to name our stuff after Greek mythology)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ arion-compose.cabal view
@@ -0,0 +1,84 @@+cabal-version:       2.4++name:                arion-compose+version: 0.1.0.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+-- bug-reports:+license:             Apache-2.0+license-file:        LICENSE+author:              Robert Hensing+maintainer:          robert@hercules-ci.com+-- copyright:+category:            Distribution, Network, Cloud, Distributed Computing+extra-source-files:  CHANGELOG.md, README.asciidoc,+                     src/haskell/testdata/**/*.nix+                     src/haskell/testdata/**/*.json+data-files:          nix/*.nix+                   , nix/modules/composition/*.nix+                   , nix/modules/nixos/*.nix+                   , nix/modules/service/*.nix+                   , nix/modules/lib/*.nix++-- all data is verbatim from some sources+data-dir:            src++common common+  build-depends:     base ^>=4.12.0.0+                   , aeson+                   , aeson-pretty+                   , async+                   , bytestring+                   , directory+                   , lens+                   , lens-aeson+                   , process+                   , temporary+                   , text+                   , protolude+                   , unix+  ghc-options:     -Wall++flag ghci+  default: False+  manual: True++library+  import:              common+  exposed-modules:     Arion.Nix+                       Arion.Aeson+                       Arion.DockerCompose+                       Arion.Images+                       Arion.Services+  other-modules:       Paths_arion_compose+  autogen-modules:     Paths_arion_compose+  -- other-extensions:+  hs-source-dirs:      src/haskell/lib+  default-language:    Haskell2010++executable arion+  import:              common+  main-is:             Main.hs+  -- other-modules:+  -- other-extensions:+  build-depends:       optparse-applicative+                     , arion-compose+  hs-source-dirs:      src/haskell/exe+  default-language:    Haskell2010++test-suite arion-unit-tests+  import:              common+  if flag(ghci)+    hs-source-dirs:      src/haskell/lib+    ghc-options:       -Wno-missing-home-modules+  type:                exitcode-stdio-1.0+  main-is:             TestMain.hs+  other-modules:       Spec+                     , Arion.NixSpec+  -- other-extensions:+  build-depends:       arion-compose+                     , hspec+                     , QuickCheck+  hs-source-dirs:      src/haskell/test+  default-language:    Haskell2010
+ src/haskell/exe/Main.hs view
@@ -0,0 +1,265 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE OverloadedStrings #-}++import Protolude hiding (Down, option)++import           Arion.Nix+import           Arion.Aeson+import           Arion.Images (loadImages)+import qualified Arion.DockerCompose as DockerCompose+import           Arion.Services (getDefaultExec)++import Options.Applicative+import Control.Monad.Fail++import qualified Data.Text as T+import qualified Data.Text.IO as T++import Data.List.NonEmpty (NonEmpty(..))++import System.Posix.User (getRealUserID)++data CommonOptions =+  CommonOptions+    { files :: NonEmpty FilePath+    , pkgs :: Text+    , nixArgs :: [Text]+    }+  deriving (Show)++newtype DockerComposeArgs =+  DockerComposeArgs { unDockerComposeArgs :: [Text] }++ensureConfigFile :: [FilePath] -> NonEmpty FilePath+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.")+    -- 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 :: Mod ArgumentFields [Char] -> Parser Text+textArgument = fmap T.pack . strArgument++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:"+    )++parseAll :: Parser (IO ())+parseAll =+  flip ($) <$> parseOptions <*> parseCommand++parseDockerComposeArgs :: Parser DockerComposeArgs+parseDockerComposeArgs =+  DockerComposeArgs <$>+    many (argument (T.pack <$> str) (metavar "DOCKER-COMPOSE ARGS..."))++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))+++--------------------------------------------------------------------------------++runDC :: Text -> DockerComposeArgs -> CommonOptions -> IO ()+runDC cmd (DockerComposeArgs args) _opts = do+  DockerCompose.run DockerCompose.Args+    { files = []+    , otherArgs = [cmd] ++ args+    }++runBuildAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO ()+runBuildAndDC cmd dopts opts = do+  ea <- defaultEvaluationArgs opts+  Arion.Nix.withBuiltComposition ea $ \path -> do+    loadImages path+    DockerCompose.run DockerCompose.Args+      { files = [path]+      , otherArgs = [cmd] ++ unDockerComposeArgs dopts+      }++runEvalAndDC :: Text -> DockerComposeArgs -> CommonOptions -> IO ()+runEvalAndDC cmd dopts opts = do+  ea <- defaultEvaluationArgs opts+  Arion.Nix.withEvaluatedComposition ea $ \path ->+    DockerCompose.run DockerCompose.Args+      { files = [path]+      , otherArgs = [cmd] ++ unDockerComposeArgs dopts+      }++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+    }++runCat :: CommonOptions -> IO ()+runCat co = do+  v <- Arion.Nix.evaluateComposition =<< defaultEvaluationArgs co+  T.hPutStrLn stdout (pretty v)++runRepl :: CommonOptions -> IO ()+runRepl co = do+  putErrText+    "Launching a repl for you. To get started:\n\+    \\n\+    \To see deployment-wide configuration\n\+    \  type config. and use tab completion\n\+    \To bring the top-level Nixpkgs attributes into scope\n\+    \  type :a (config._module.args.pkgs) // { inherit config; }\n\+    \"+  Arion.Nix.replForComposition =<< defaultEvaluationArgs co++detachFlag :: Parser Bool+detachFlag = flag False True (long "detach" <> short 'd' <> help "Detached mode: Run command in the background.")++privilegedFlag :: Parser Bool+privilegedFlag = flag False True (long "privileged" <> help "Give extended privileges to the process.")++userOption :: Parser Text+userOption = strOption (long "user" <> short 'u' <> help "Run the command as this user.")++noTTYFlag :: Parser Bool+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.")++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)++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")+          <*> many (argument (T.pack <$> str) (metavar "ARG"))+  )++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++  ea <- defaultEvaluationArgs opts+  Arion.Nix.withEvaluatedComposition ea $ \path -> do+    commandAndArgs'' <- case commandAndArgs of+      [] -> getDefaultExec path service+      x -> pure x+    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 = args+      }++main :: IO ()+main = +  (join . arionExecParser) (info (parseAll <**> helper) fullDesc)+  where+    arionExecParser = customExecParser (prefs showHelpOnEmpty)+
+ src/haskell/lib/Arion/Aeson.hs view
@@ -0,0 +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 qualified Data.Aeson.Encode.Pretty+import           Data.Aeson.Encode.Pretty       ( defConfig+                                                , 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 }++decodeFile :: FromJSON a => FilePath -> IO a+decodeFile fp = do+  b <- BL.readFile fp+  case eitherDecode b of+    Left e -> panic (toS e)+    Right v -> pure v
+ src/haskell/lib/Arion/DockerCompose.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+module Arion.DockerCompose where++import           Prelude                        ( )+import           Protolude+import           System.Process++data Args = Args+  { files :: [FilePath]+  , otherArgs :: [Text]+  }++run :: Args -> IO ()+run args = do+  let fileArgs = files args >>= \f -> ["--file", f]+      allArgs  = fileArgs ++ map toS (otherArgs args)++      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+      ExitSuccess -> pass+      ExitFailure 1 -> exitFailure+      ExitFailure {} -> do+        throwIO $ FatalError $ "docker-compose failed with " <> show exitCode
+ src/haskell/lib/Arion/Images.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE OverloadedStrings #-}+module Arion.Images +  ( loadImages+  ) where++import Prelude()+import Protolude hiding (to)++import qualified Data.Aeson as Aeson+import           Arion.Aeson (decodeFile)+import qualified System.Process as Process++import Control.Lens+import Data.Aeson.Lens+import Data.String+import System.IO (withFile, IOMode(ReadMode))+++data Image = Image+  { image :: Text -- ^ file path+  , imageName :: Text+  , imageTag :: Text+  } deriving (Generic, Aeson.ToJSON, Aeson.FromJSON, Show)++type TaggedImage = Text++-- | Subject to change+loadImages :: FilePath -> IO ()+loadImages fp = do++  v <- decodeFile fp++  loaded <- dockerImages++  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 . map (toS . image) . filter isNew $ images++loadImage :: FilePath -> IO ()+loadImage imgPath = withFile (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 (" <> show code <> ") failed for " <> toS imgPath+++dockerImages :: IO [TaggedImage]+dockerImages = do+  let procSpec = Process.proc "docker" [ "images",  "--filter", "dangling=false", "--format", "{{.Repository}}:{{.Tag}}" ]+  (map toS . lines) <$> Process.readCreateProcess procSpec ""
+ src/haskell/lib/Arion/Nix.hs view
@@ -0,0 +1,184 @@+{-# LANGUAGE OverloadedStrings #-}+module Arion.Nix+  ( evaluateComposition+  , withEvaluatedComposition+  , buildComposition+  , withBuiltComposition+  , replForComposition+  , EvaluationArgs(..)+  , EvaluationMode(..)+  ) where++import           Prelude                        ( )+import           Protolude+import           Arion.Aeson                    ( pretty )+import           Data.Aeson+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           Data.List.NonEmpty             ( NonEmpty(..) )++import           Control.Arrow                  ( (>>>) )+import           System.IO.Temp                 ( withTempFile )+import           System.IO                      ( hClose )++data EvaluationMode =+  ReadWrite | ReadOnly++data EvaluationArgs = EvaluationArgs+ { evalUid :: Int+ , evalModules :: NonEmpty FilePath+ , evalPkgs :: Text+ , evalWorkDir :: Maybe FilePath+ , evalMode :: EvaluationMode+ , evalUserArgs :: [Text]+ }++evaluateComposition :: EvaluationArgs -> IO Value+evaluateComposition ea = do+  evalComposition <- getEvalCompositionFile+  let commandArgs =+        [ "--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+        }+  +  withCreateProcess procSpec $ \_in outHM _err procHandle -> do+    let outHandle = fromMaybe (panic "stdout missing") outHM++    out <- BL.hGetContents outHandle++    v <- Protolude.evaluate (eitherDecode out)++    exitCode <- waitForProcess procHandle++    case exitCode of+      ExitSuccess -> pass+      ExitFailure 1 -> exitFailure+      ExitFailure {} -> do+        throwIO $ FatalError $ "evaluation failed with " <> show exitCode++    case v of+      Right r -> pure r+      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+withEvaluatedComposition ea f = do+  v <- evaluateComposition ea+  withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path yamlHandle -> do+    T.hPutStrLn yamlHandle (pretty v)+    hClose yamlHandle+    f path+++buildComposition :: FilePath -> EvaluationArgs -> IO ()+buildComposition outLink ea = do+  evalComposition <- getEvalCompositionFile+  let commandArgs =+        [ "--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++    exitCode <- waitForProcess procHandle++    case exitCode of+      ExitSuccess -> pass+      ExitFailure 1 -> exitFailure+      ExitFailure {} -> do+        throwIO $ FatalError $ "nix-build failed with " <> show exitCode++-- | Do something with a docker-compose.yaml.+withBuiltComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r+withBuiltComposition ea f = do+  withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path emptyYamlHandle -> do+    hClose emptyYamlHandle+    -- Known problem: kills atomicity of withTempFile; won't fix because we should manage gc roots,+    -- impl of which will probably avoid this "problem". It seems unlikely to cause issues.+    Directory.removeFile path+    buildComposition path ea+    f path+++replForComposition :: EvaluationArgs -> IO ()+replForComposition ea = do+    evalComposition <- getEvalCompositionFile+    let args =+          [ "repl", evalComposition ]+          ++ argArgs ea+          ++ map toS (evalUserArgs ea)+        procSpec = (proc "nix" args) { cwd = evalWorkDir ea }+    +    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++argArgs :: EvaluationArgs -> [[Char]]+argArgs ea =+      [ "--argstr"+      , "uid"+      , show $ evalUid ea+      , "--arg"+      , "modules"+      , modulesNixExpr $ evalModules ea+      , "--arg"+      , "pkgs"+      , toS $ evalPkgs ea+      ]++getEvalCompositionFile :: IO FilePath+getEvalCompositionFile = getDataFileName "nix/eval-composition.nix"++modeArguments :: EvaluationMode -> [[Char]]+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 <> "}\")"++  isAbsolute ('/' : _) = True+  isAbsolute _         = False++  wrapList s = "[ " <> s <> " ]"++toNixStringLiteral :: [Char] -> [Char]+toNixStringLiteral = show -- FIXME: custom escaping including '$'
+ src/haskell/lib/Arion/Services.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE OverloadedStrings #-}+module Arion.Services+  ( getDefaultExec+  ) where++import Prelude()+import Protolude hiding (to)++import qualified Data.Aeson as Aeson+import           Arion.Aeson (decodeFile)++import Control.Lens+import Data.Aeson.Lens++-- | 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 service . key "defaultExec" . _Array . traverse . _String)
+ src/haskell/test/Arion/NixSpec.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}+module Arion.NixSpec+  ( 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++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> {}"+    , 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 = 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)++-- 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) -> prefix <> T.take 1 tl <> censorImages (T.drop 1 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 _ = False -- WRONG?
+ src/haskell/test/Spec.hs view
@@ -0,0 +1,11 @@+module Spec+  ( spec+  )+where++import           Test.Hspec+import qualified Arion.NixSpec++spec :: Spec+spec = do+  describe "Arion.Nix" Arion.NixSpec.spec
+ src/haskell/test/TestMain.hs view
@@ -0,0 +1,10 @@+module Main where++import           Prelude()+import           Protolude+import           Test.Hspec.Runner+import qualified Spec++main :: IO ()+main = hspecWith config Spec.spec+  where config = defaultConfig { configColorMode = ColorAlways }
+ src/haskell/testdata/Arion/NixSpec/arion-compose.json view
@@ -0,0 +1,49 @@+{+    "services": {+        "webserver": {+            "command": [+                "/nix/store/b9w61w4g8sqgrm3rid6ca22krslqghb3-nixos-system-unnamed-19.03.173100.e726e8291b2/init"+            ],+            "environment": {+                "NIX_REMOTE": "",+                "PATH": "/usr/bin:/run/current-system/sw/bin/",+                "container": "docker"+            },+            "image": "arion-base:<HASH>",+            "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:ro",+                "/nix/store/pssdmhzjnhflawv7rwk1yw39350iv40g-container-system-env:/run/system:ro"+            ]+        }+    },+    "version": "3.4",+    "x-arion": {+        "images": [+            {+                "image": "<STOREPATH>",+                "imageName": "arion-base",+                "imageTag": "<HASH>"+            }+        ],+        "serviceInfo": {+            "webserver": {+                "defaultExec": [+                    "/run/current-system/sw/bin/bash",+                    "-l"+                ]+            }+        }+    }+}
+ src/haskell/testdata/Arion/NixSpec/arion-compose.nix view
@@ -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+    ];+  };+}
+ src/haskell/testdata/docker-compose-example.json view
@@ -0,0 +1,42 @@+{+  "services": {+    "webserver": {+      "environment": {+        "container": "docker"+      },+      "image": "webserver:xr4ljmz3qfcwlq9rl4mr4qdrzw93rl70",+      "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"+      ]+    }+  },+  "version": "3.4",+  "x-arion": {+    "images": [+      {+        "image": "/nix/store/xr4ljmz3qfcwlq9rl4mr4qdrzw93rl70-docker-image-webserver.tar.gz",+        "imageName": "webserver",+        "imageTag": "xr4ljmz3qfcwlq9rl4mr4qdrzw93rl70"+      }+    ],+    "serviceInfo": {+      "webserver": {+        "defaultExec": [+          "/run/current-system/sw/bin/bash",+          "-l"+        ]+      }+    }+  }+}
+ src/nix/eval-composition.nix view
@@ -0,0 +1,42 @@+{ modules ? [], uid ? "0", pkgs, hostNixStorePrefix ? "", }:++let _pkgs = pkgs;+in+let+  pkgs = if builtins.typeOf _pkgs == "path"+         then import _pkgs+         else if builtins.typeOf _pkgs == "set"+         then _pkgs+         else builtins.abort "The pkgs argument must be an attribute set or a path to an attribute set.";++  inherit (pkgs) lib;++  composition = lib.evalModules {+    check = true;+    modules = builtinModules ++ modules;+  };++  builtinModules = [+    argsModule+    ./modules/composition/docker-compose.nix+    ./modules/composition/host-environment.nix+    ./modules/composition/images.nix+    ./modules/composition/service-info.nix+    ./modules/composition/arion-base-image.nix+  ];++  argsModule = {+    _file = ./eval-composition.nix;+    key = ./eval-composition.nix;+    config._module.args.pkgs = lib.mkIf (pkgs != null) (lib.mkForce pkgs);+    config.host.nixStorePrefix = hostNixStorePrefix;+    config.host.uid = lib.toInt uid;+  };++in+  # Typically you need composition.config.out.dockerComposeYaml+  composition // {+    # throw in lib and pkgs for repl convenience+    inherit lib;+    inherit (composition.config._module.args) pkgs;+  }
+ src/nix/modules/composition/arion-base-image.nix view
@@ -0,0 +1,41 @@+++# This module is subject to change.+# In particular, arion-base should use a generic non-service image building system++{ config, lib, pkgs, ... }: ++let++  tag = lib.head (lib.strings.splitString "-" (baseNameOf builtImage.outPath));+  name = "arion-base";++  builtImage = pkgs.dockerTools.buildLayeredImage {+    inherit name;+    contents = pkgs.runCommand "minimal-contents" {} ''+        mkdir -p $out/bin $out/usr/bin+        ln -s /run/system/bin/sh $out/bin/sh+        ln -s /run/system/usr/bin/env $out/usr/bin/env+      '';+    config = {};+  };++in++{++  options = {+    arionBaseImage = lib.mkOption {+      type = lib.types.str;+      description = "Image to use when using useHostStore. Don't use this option yourself. It's going away.";+      internal = true;+    };+  };++  config = {+    arionBaseImage = "${name}:${tag}";+    build.imagesToLoad = lib.mkIf (lib.any (s: s.service.useHostStore) (lib.attrValues config.services)) [+      { image = builtImage; imageName = name; imageTag = tag; }+    ];  +  };+}
+ src/nix/modules/composition/docker-compose.nix view
@@ -0,0 +1,78 @@+/*++   This is a composition-level module.++   It defines the low-level options that are read by arion, like+    - out.dockerComposeYaml++   It declares options like+    - services++ */+compositionArgs@{ lib, config, options, pkgs, ... }:+let+  inherit (lib) types;++  service = {+    imports = [ argsModule ] ++ import ../service/all-modules.nix;+  };+  argsModule =+    { name, # injected by types.submodule+      ...+    }: {+      _file = ./docker-compose.nix;+      key = ./docker-compose.nix;++      config._module.args.pkgs = lib.mkDefault compositionArgs.pkgs;+      config.host = compositionArgs.config.host;+      config.composition = compositionArgs.config;+      config.service.name = name;+    };++in+{+  imports = [+    ../lib/assert.nix+    (lib.mkRenamedOptionModule ["docker-compose" "services"] ["services"])+  ];+  options = {+    out.dockerComposeYaml = lib.mkOption {+      type = lib.types.package;+      description = "A derivation that produces a docker-compose.yaml file for this composition.";+      readOnly = true;+    };+    out.dockerComposeYamlText = lib.mkOption {+      type = lib.types.str;+      description = "The text of out.dockerComposeYaml.";+      readOnly = true;+    };+    out.dockerComposeYamlAttrs = lib.mkOption {+      type = lib.types.attrsOf lib.types.unspecified;+      description = "The text of out.dockerComposeYaml.";+      readOnly = true;+    };+    docker-compose.raw = lib.mkOption {+      type = lib.types.attrs;+      description = "Attribute set that will be turned into the docker-compose.yaml file, using Nix's toJSON builtin.";+    };+    docker-compose.extended = lib.mkOption {+      type = lib.types.attrs;+      description = "Attribute set that will be turned into the x-arion section of the docker-compose.yaml file.";+    };+    services = lib.mkOption {+      type = lib.types.attrsOf (lib.types.submodule service);+      description = "An attribute set of service configurations. A service specifies how to run an image as a container.";+    };+  };+  config = {+    out.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" config.out.dockerComposeYamlText;+    out.dockerComposeYamlText = builtins.toJSON (config.out.dockerComposeYamlAttrs);+    out.dockerComposeYamlAttrs = config.assertWarn config.docker-compose.raw;++    docker-compose.raw = {+      version = "3.4";+      services = lib.mapAttrs (k: c: c.out.service) config.services;+      x-arion = config.docker-compose.extended;+    };+  };+}
+ src/nix/modules/composition/host-environment.nix view
@@ -0,0 +1,33 @@+{ lib, ... }:++{+  options = {++    host.uid = lib.mkOption {+      type = lib.types.int;+      description = ''+        The numeric user id (UID) of the user running arion.++        This lets you to write modules that interact with the host+        user's files, which is helpful for local development, but not+        intended for production-like deployment scenarios.+      '';+    };++    host.nixStorePrefix = lib.mkOption {+      type = lib.types.str;+      default = "";+      example = "/mnt/foo";+      description = ''+        Prefixes store paths on the host, allowing the Nix store to be+        stored at an alternate location without altering the format of+        store paths.++        For example: instead of mounting the host's /nix/store as the+        container's /nix/store, this will mount /mnt/foo/nix/store+        as the container's /nix/store.+      '';+    };++  };+}
+ src/nix/modules/composition/images.nix view
@@ -0,0 +1,39 @@+{ pkgs, lib, config, ... }:+let+  inherit (lib.types) listOf package unspecified;++  serviceImages =+    lib.mapAttrs addDetails (+      lib.filterAttrs filterFunction config.services+    );++  filterFunction = serviceName: service:+    builtins.addErrorContext "while evaluating whether the service ${serviceName} defines an image"+      service.image.nixBuild;++  addDetails = serviceName: service:+    builtins.addErrorContext "while evaluating the image for service ${serviceName}"+    (let+      inherit (service) build;+    in {+      image = build.image.outPath;+      imageName = build.imageName or service.image.name;+      imageTag =+                 if build.image.imageTag != ""+                 then build.image.imageTag+                 else lib.head (lib.strings.splitString "-" (baseNameOf build.image.outPath));+    });+in+{+  options = {+    build.imagesToLoad = lib.mkOption {+      type = listOf unspecified;+      internal = true;+      description = "List of dockerTools image derivations.";+    };+  };+  config = {+    build.imagesToLoad = lib.attrValues serviceImages;+    docker-compose.extended.images = config.build.imagesToLoad;+  };+}
+ src/nix/modules/composition/service-info.nix view
@@ -0,0 +1,21 @@+/*+   Adds extendedInfo from services to the Docker Compose file.++   This contains fields that are not in Docker Compose schema.+ */+{ config, lib, ... }:+let+  inherit (lib) mapAttrs filterAttrs;++  serviceInfo =+    filterAttrs (_k: v: v != {})+      (mapAttrs (_serviceName: service: service.out.extendedInfo)+        config.services+      );++in+{+  config = {+    docker-compose.extended.serviceInfo = serviceInfo;+  };+}
+ src/nix/modules/lib/assert.nix view
@@ -0,0 +1,32 @@+{ config, lib, pkgs, ... }:++# based on nixpkgs/nixos/modules/system/activation/top-level.nix++let+  inherit (lib) filter concatStringsSep types mkOption;++  # lib.showWarnings since 19.09+  showWarnings = warnings: res: lib.fold (w: x: lib.warn w x) res warnings;+  warn = msg: builtins.trace "[1;31mwarning: ${msg}[0m";++  # Handle assertions and warnings++  failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);++  assertWarn = if failedAssertions != []+    then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"+    else showWarnings config.warnings;++in++{+  imports = [ ./assertions.nix ];+  options.assertWarn = mkOption {+    type = types.unspecified; # a function+    # It's for the wrapping program to know about this. User need not care.+    internal = true;+    readOnly = true;+  };+  config = { inherit assertWarn; };+}+
+ src/nix/modules/lib/assertions.nix view
@@ -0,0 +1,34 @@+{ lib, ... }:++with lib;++{++  options = {++    assertions = mkOption {+      type = types.listOf types.unspecified;+      internal = true;+      default = [];+      example = [ { assertion = false; message = "you can't enable this for that reason"; } ];+      description = ''+        This option allows modules to express conditions that must+        hold for the evaluation of the system configuration to+        succeed, along with associated error messages for the user.+      '';+    };++    warnings = mkOption {+      internal = true;+      default = [];+      type = types.listOf types.str;+      example = [ "The `foo' service is deprecated and will go away soon!" ];+      description = ''+        This option allows modules to show warnings to users during+        the evaluation of the system configuration.+      '';+    };++  };+  # impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix>+}
+ src/nix/modules/nixos/container-systemd.nix view
@@ -0,0 +1,24 @@+/*+  NixOS configuration to for running a mostly normal systemd-based+  NixOS in Docker.+ */+{ pkgs, lib, ...}: {++  # imports = [+  #   # This profile doesn't seem to work well.+  #   (pkgs.path + "/nixos/modules/profiles/docker-container.nix")+  #   This one works, but can not be imported here due because imports can not depend on pkgs.+  #   (pkgs.path + "/nixos/modules/profiles/minimal.nix")+  # ];+  boot.isContainer = true;+  boot.specialFileSystems = lib.mkForce {};+  networking.hostName = "";++  services.journald.console = "/dev/console";++  systemd.services.systemd-logind.enable = false;+  systemd.services.console-getty.enable = false;++  systemd.sockets.nix-daemon.enable = lib.mkDefault false;+  systemd.services.nix-daemon.enable = lib.mkDefault false;+}
+ src/nix/modules/nixos/default-shell.nix view
@@ -0,0 +1,4 @@+{ config, utils, ... }:+{+  config.system.build.x-arion-defaultShell = utils.toShellPath config.users.defaultUserShell;+}
+ src/nix/modules/service/all-modules.nix view
@@ -0,0 +1,11 @@+[+  ./default-exec.nix+  ./docker-compose-service.nix+  ./extended-info.nix+  ./host-store.nix+  ./context.nix+  ./image.nix+  ./nixos.nix+  ./nixos-init.nix+  ../lib/assert.nix+]
+ src/nix/modules/service/context.nix view
@@ -0,0 +1,19 @@+{ lib, ... }:+{+  options = {+    host = lib.mkOption {+      type = lib.types.attrs;+      readOnly = true;+      description = ''+        The composition-level host option values.+      '';+    };+    composition = lib.mkOption {+      type = lib.types.attrs;+      readOnly = true;+      description = ''+        The composition configuration.+      '';+    };+  };+}
+ src/nix/modules/service/default-exec.nix view
@@ -0,0 +1,19 @@+{ config, lib, ... }:+let+  inherit (lib) types mkOption;+in+{+  options = {+    service.defaultExec = mkOption {+      type = types.listOf types.str;+      default = ["/bin/sh"];+      description = ''+        Container program and arguments to invoke when calling+        <code>arion exec &lt;service.name></code> without further arguments.+      '';+    };+  };+  config = {+    out.extendedInfo.defaultExec = config.service.defaultExec;+  };+}
+ src/nix/modules/service/docker-compose-service.nix view
@@ -0,0 +1,273 @@+/*++   This service-level module defines the out.service option, using+   the user-facing options service.image, service.volumes, etc.++ */+{ pkgs, lib, config, ... }:++let+  inherit (lib) mkOption types;+  inherit (types) listOf nullOr attrsOf str either int bool;++  link = url: text:+    ''<link xlink:href="${url}">${text}</link>'';+  dockerComposeRef = fragment:+    ''See <link xlink:href="https://docs.docker.com/compose/compose-file/#${fragment}">Docker Compose#${fragment}</link>'';+  dockerComposeKitchenSink = ''+    Analogous to the <code>docker run</code> counterpart.++    ${dockerComposeRef "domainname-hostname-ipc-mac_address-privileged-read_only-shm_size-stdin_open-tty-user-working_dir"}+  '';++  cap_add = lib.attrNames (lib.filterAttrs (name: value: value == true) config.service.capabilities);+  cap_drop = lib.attrNames (lib.filterAttrs (name: value: value == false) config.service.capabilities);++in+{+  imports = [+    (lib.mkRenamedOptionModule ["build" "service"] ["out" "service"])+  ];++  options = {+    out.service = mkOption {+      type = attrsOf types.unspecified;+      description = ''+        Raw input for the service in <code>docker-compose.yaml</code>.++        You should not need to use this option. If anything is+        missing, please contribute the missing option.++        This option is user accessible because it may serve as an+        escape hatch for some.+      '';+      apply = config.assertWarn;+    };++    service.name = mkOption {+      type = str;+      description = ''+        The name of the service - <code>&lt;name></code> in the composition-level <code>services.&lt;name></code>+      '';+      readOnly = true;+    };++    service.volumes = mkOption {+      type = listOf types.unspecified;+      default = [];+      description = dockerComposeRef "volumes";+    };+    service.tmpfs = mkOption {+      type = listOf types.str;+      default = [];+      description = dockerComposeRef "tmpfs";+    };+    service.build.context = mkOption {+      type = nullOr str;+      default = null;+      description = ''+        Locates a Dockerfile to use for creating an image to use in this service.++        ${dockerComposeRef "context"}+      '';+    };+    service.hostname = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeKitchenSink;+    };+    service.tty = mkOption {+      type = nullOr bool;+      default = null;+      description = dockerComposeKitchenSink;+    };+    service.environment = mkOption {+      type = attrsOf (either str int);+      default = {};+      description = dockerComposeRef "environment";+    };+    service.image = mkOption {+      type = str;+      description = dockerComposeRef "image";+    };+    service.command = mkOption {+      type = nullOr types.unspecified;+      default = null;+      description = dockerComposeRef "command";+    };+    service.container_name = mkOption {+      type = nullOr types.str;+      default = null;+      description = dockerComposeRef "container_name";+    };+    service.depends_on = mkOption {+      type = listOf str;+      default = [];+      description = dockerComposeRef "depends_on";+    };+    service.devices = mkOption {+      type = listOf str;+      default = [];+      description = ''+        See ${link "https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities"+        "<code>docker run --device</code> documentation"}++        ${dockerComposeRef "devices"}+      '';+    };+    service.links = mkOption {+      type = listOf str;+      default = [];+      description = dockerComposeRef "links";+    };+    service.external_links = mkOption {+      type = listOf str;+      default = [];+      description = dockerComposeRef "external_links";+    };+    service.extra_hosts = mkOption {+      type = listOf str;+      default = [];+      description = dockerComposeRef "extra_hosts";+    };+    service.working_dir = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeKitchenSink;+    };+    service.privileged = mkOption {+      type = nullOr bool;+      default = null;+      description = dockerComposeKitchenSink;+    };+    service.entrypoint = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeRef "entrypoint";+    };+    service.restart = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeRef "restart";+    };+    service.user = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeKitchenSink;+    };+    service.ports = mkOption {+      type = listOf types.unspecified;+      default = [];+      description = ''+        Expose ports on host. "host:container" or structured.++        ${dockerComposeRef "ports"}+      '';+    };+    service.expose = mkOption {+      type = listOf str;+      default = [];+      description = dockerComposeRef "expose";+    };+    service.env_file = mkOption {+      type = listOf str;+      default = [];+      description = dockerComposeRef "env_file";+    };+    service.network_mode = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeRef "network_mode";+    };+    service.networks = mkOption {+      type = nullOr (listOf types.str);+      default = null;+      description = dockerComposeRef "networks";+    };+    service.stop_signal = mkOption {+      type = nullOr str;+      default = null;+      description = dockerComposeRef "stop_signal";+    };+    service.sysctls = mkOption {+      type = attrsOf (either str int);+      default = {};+      description = dockerComposeRef "sysctls";+    };+    service.capabilities = mkOption {+      type = attrsOf (nullOr bool);+      default = {};+      example = { ALL = true; SYS_ADMIN = false; NET_ADMIN = false; };+      description = ''+        Enable/disable linux capabilities, or pick Docker's default.++        Setting a capability to <code>true</code> means that it will be+        "added". Setting it to <code>false</code> means that it will be "dropped".+        ${dockerComposeRef "cap_add-cap_drop"}++        Omitted and <code>null</code> capabilities will therefore be set+        according to Docker's ${+          link "https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities"+               "default list of capabilities."+        }+      '';+    };+  };++  config.out.service = {+    inherit (config.service)+      volumes+      environment+      sysctls+      image+      ;+  } // lib.optionalAttrs (config.service.build.context != null) {+    inherit (config.service) build;+  } // lib.optionalAttrs (cap_add != []) {+    inherit cap_add;+  } // lib.optionalAttrs (cap_drop != []) {+    inherit cap_drop;+  } // lib.optionalAttrs (config.service.command != null) {+    inherit (config.service) command;+  } // lib.optionalAttrs (config.service.container_name != null) {+    inherit (config.service) container_name;+  } // lib.optionalAttrs (config.service.depends_on != []) {+    inherit (config.service) depends_on;+  } // lib.optionalAttrs (config.service.devices != []) {+    inherit (config.service) devices;+  } // lib.optionalAttrs (config.service.entrypoint != null) {+    inherit (config.service) entrypoint;+  } // lib.optionalAttrs (config.service.env_file != []) {+    inherit (config.service) env_file;+  } // lib.optionalAttrs (config.service.expose != []) {+    inherit (config.service) expose;+  } // lib.optionalAttrs (config.service.external_links != []) {+    inherit (config.service) external_links;+  } // lib.optionalAttrs (config.service.extra_hosts != []) {+    inherit (config.service) extra_hosts;+  } // lib.optionalAttrs (config.service.hostname != null) {+    inherit (config.service) hostname;+  } // lib.optionalAttrs (config.service.links != []) {+    inherit (config.service) links;+  } // lib.optionalAttrs (config.service.ports != []) {+    inherit (config.service) ports;+  } // lib.optionalAttrs (config.service.privileged != null) {+    inherit (config.service) privileged;+  } // lib.optionalAttrs (config.service.network_mode != null) {+    inherit (config.service) network_mode;+  } // lib.optionalAttrs (config.service.networks != null) {+    inherit (config.service) networks;+  } // lib.optionalAttrs (config.service.restart != null) {+    inherit (config.service) restart;+  } // lib.optionalAttrs (config.service.stop_signal != null) {+    inherit (config.service) stop_signal;+  } // lib.optionalAttrs (config.service.tmpfs != []) {+    inherit (config.service) tmpfs;+  } // lib.optionalAttrs (config.service.tty != null) {+    inherit (config.service) tty;+  } // lib.optionalAttrs (config.service.working_dir != null) {+    inherit (config.service) working_dir;+  } // lib.optionalAttrs (config.service.user != null) {+    inherit (config.service) user;+  };+}
+ src/nix/modules/service/extended-info.nix view
@@ -0,0 +1,23 @@+{ config, lib, ... }:+let+  inherit (lib) mkOption;+  inherit (lib.types) attrsOf unspecified;+in+{+  imports = [+    (lib.mkRenamedOptionModule ["build" "extendedInfo"] ["out" "extendedInfo"])+  ];+  options = {+    out.extendedInfo = mkOption {+      type = attrsOf unspecified;+      description = ''+        Information about a service to include in the Docker Compose file,+        but that will not be used by the <code>docker-compose</code> command+        itself.++        It will be inserted in <code>x-arion.serviceInfo.&lt;service.name></code>.+      '';+      default = {};+    };+  };+}
+ src/nix/modules/service/host-store.nix view
@@ -0,0 +1,41 @@+/*++   This service-level bind mounts the host store into the container+   when the service.useHostStore option is set to true.++ */+{ lib, config, pkgs, ... }:++let+  inherit (lib) mkOption types mkIf;+  escape = s: lib.replaceStrings ["$"] ["$$"] s;+in+{+  options = {+    service.useHostStore = mkOption {+      type = types.bool;+      default = false;+      description = "Bind mounts the host store if enabled, avoiding copying.";+    };+    service.hostStoreAsReadOnly = mkOption {+      type = types.bool;+      default = true;+      description = "Adds a ':ro' (read-only) access mode to the host nix store bind mount.";+    };+    service.useHostNixDaemon = mkOption {+      type = types.bool;+      default = false;+      description = "Make the host Nix daemon available.";+    };+  };+  config = mkIf config.service.useHostStore {+    image.nixBuild = false; # no need to build and load+    service.image = config.composition.arionBaseImage;+    service.environment.NIX_REMOTE = lib.optionalString config.service.useHostNixDaemon "daemon";+    service.volumes = [+      "${config.host.nixStorePrefix}/nix/store:/nix/store${lib.optionalString config.service.hostStoreAsReadOnly ":ro"}"+      "${config.host.nixStorePrefix}${pkgs.buildEnv { name = "container-system-env"; paths = [ pkgs.bashInteractive pkgs.coreutils ]; }}:/run/system${lib.optionalString config.service.hostStoreAsReadOnly ":ro"}"+      ] ++ lib.optional config.service.useHostNixDaemon "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket";+    service.command = lib.mkDefault (map escape (config.image.rawConfig.Cmd or []));+  };+}
+ src/nix/modules/service/image.nix view
@@ -0,0 +1,122 @@+{ pkgs, lib, config, options, ... }:+let+  inherit (lib) types mkOption;+  inherit (types) attrsOf listOf nullOr package str unspecified bool;++  # TODO: dummy-config is a useless layer. Nix 2.3 will let us inspect+  #       the string context instead, so we can avoid this.+  contentsList = config.image.contents ++ [+    (pkgs.writeText "dummy-config.json" (builtins.toJSON config.image.rawConfig))+  ];++  builtImage = pkgs.dockerTools.buildLayeredImage {+    inherit (config.image)+      name+      contents+      ;+    config = config.image.rawConfig;+    maxLayers = 100;++    # TODO: allow use of image's Nix package instead+    # TODO: option to disable db generation+    extraCommands = ''+        echo "Generating the nix database..."+        echo "Warning: only the database of the deepest Nix layer is loaded."+        echo "         If you want to use nix commands in the container, it would"+        echo "         be better to only have one layer that contains a nix store."+        export NIX_REMOTE=local?root=$PWD+        ${pkgs.nix}/bin/nix-store --load-db < ${pkgs.closureInfo {rootPaths = contentsList;}}/registration+        mkdir -p nix/var/nix/gcroots/docker/+        for i in ${lib.concatStringsSep " " contentsList}; do+          ln -s $i nix/var/nix/gcroots/docker/$(basename $i)+        done;+      '';+  };++  priorityIsDefault = option: option.highestPrio >= (lib.mkDefault true).priority;+in+{+  options = {+    build.image = mkOption {+      type = nullOr package;+      description = ''+        Docker image derivation to be <code>docker load</code>ed.+      '';+      internal = true;+    };+    build.imageName = mkOption {+      type = str;+      description = "Derived from build.image";+      internal = true;+    };+    build.imageTag = mkOption {+      type = str;+      description = "Derived from build.image";+      internal = true;+    };+    image.nixBuild = mkOption {+      type = bool;+      description = ''+        Whether to build this image with Nixpkgs'+        <code>dockerTools.buildLayeredImage</code>+        and then load it with <code>docker load</code>.++        By default, an image will be built with Nix unless <option>service.image</option>+        is set. See also <option>image.name</option>, which defaults to+        the service name.+      '';+    };+    image.name = mkOption {+      type = str;+      default = config.service.name;+      defaultText = lib.literalExample "config.service.name";+      description = ''+        A human readable name for the docker image.++        Shows up in the <code>docker ps</code> output in the+        <code>IMAGE</code> column, among other places.+      '';+    };+    image.contents = mkOption {+      type = listOf package;+      default = [];+      description = ''+         Top level paths in the container.+      '';+    };+    image.rawConfig = mkOption {+      type = attrsOf unspecified;+      default = {};+      description = ''+        This is a low-level fallback for when a container option has not+        been modeled in the Arion module system.++        This attribute set does not have an appropriate merge function.+        Please use the specific <code>image</code> options instead.++        Run-time configuration of the container. A full list of the+        options are available at in the <link xlink:href="https://github.com/moby/moby/blob/master/image/spec/v1.2.md#image-json-field-descriptions">Docker Image Specification+        v1.2.0</link>.+      '';+    };+    image.command = mkOption {+      type = listOf str;+      default = [];+      description = ''+      '';+    };+  };+  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);+  };+}
+ src/nix/modules/service/nixos-init.nix view
@@ -0,0 +1,42 @@+/*+  Invokes the NixOS init system in the container.+ */+{ config, lib, pkgs, ... }:+let+  inherit (lib) types;+in+{+  options = {+    nixos.useSystemd = lib.mkOption {+      type = types.bool;+      default = false;+      description = ''+        When enabled, call the NixOS systemd-based init system.++        Configure NixOS with <code>nixos.configuration</code>.+      '';+    };+  };++  config = lib.mkIf (config.nixos.useSystemd) {+    nixos.configuration.imports = [+      ../nixos/container-systemd.nix+      ../nixos/default-shell.nix+      (pkgs.path + "/nixos/modules/profiles/minimal.nix")+    ];+    image.command = [ "${config.nixos.build.toplevel}/init" ];+    service.environment.container = "docker";+    service.environment.PATH = "/usr/bin:/run/current-system/sw/bin/";+    service.volumes = [+      "/sys/fs/cgroup:/sys/fs/cgroup:ro"+    ];+    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";++    service.stop_signal = "SIGRTMIN+3";+    service.tty = true;+    service.defaultExec = [config.nixos.build.x-arion-defaultShell "-l"];+  };+}
+ src/nix/modules/service/nixos.nix view
@@ -0,0 +1,60 @@+/*+  Generic options for adding NixOS modules and evaluating the NixOS+  configuration. The methods for installing NixOS in the image are+  defined elsewhere.+ */+{ pkgs, lib, config, ... }:+let+  inherit (lib) types;+  inherit (types) attrs;+in+{+  options = {+    nixos.configuration = lib.mkOption {+      type = with types; coercedTo unspecified (a: [a]) (listOf unspecified);+      default = {};+      description = ''+        Modules to add to the NixOS configuration.++        This option is unused by default, because not all images use NixOS.++        One way to use this is to enable <code>nixos.useSystemd</code>, but the+        NixOS configuration can be used in other ways.+      '';+    };++    nixos.build = lib.mkOption {+      type = attrs;+      readOnly = true;+      description = ''+        NixOS build products from <code>config.system.build</code>, such as <code>toplevel</code> and <code>etc</code>.++        This option is unused by default, because not all images use NixOS.++        One way to use this is to enable <code>nixos.useSystemd</code>, but the+        NixOS configuration can be used in other ways.+      '';+    };++    nixos.evaluatedConfig = lib.mkOption {+      type = attrs;+      readOnly = true;+      description = ''+        Evaluated NixOS configuration, to be read by service-level modules.++        This option is unused by default, because not all images use NixOS.++        One way to use this is to enable <code>nixos.useSystemd</code>, but the+        NixOS configuration can be used in other ways.+      '';+    };+  };++  config = {+    nixos.build = builtins.addErrorContext "while evaluating the service nixos configuration" (+      pkgs.nixos config.nixos.configuration+    );+    nixos.configuration = { config, ... }: { system.build.theConfig = config; };+    nixos.evaluatedConfig = config.nixos.build.theConfig;+  };+}