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 +9/−0
- LICENSE +202/−0
- README.asciidoc +274/−0
- Setup.hs +2/−0
- arion-compose.cabal +84/−0
- src/haskell/exe/Main.hs +265/−0
- src/haskell/lib/Arion/Aeson.hs +27/−0
- src/haskell/lib/Arion/DockerCompose.hs +30/−0
- src/haskell/lib/Arion/Images.hs +60/−0
- src/haskell/lib/Arion/Nix.hs +184/−0
- src/haskell/lib/Arion/Services.hs +23/−0
- src/haskell/test/Arion/NixSpec.hs +59/−0
- src/haskell/test/Spec.hs +11/−0
- src/haskell/test/TestMain.hs +10/−0
- src/haskell/testdata/Arion/NixSpec/arion-compose.json +49/−0
- src/haskell/testdata/Arion/NixSpec/arion-compose.nix +12/−0
- src/haskell/testdata/docker-compose-example.json +42/−0
- src/nix/eval-composition.nix +42/−0
- src/nix/modules/composition/arion-base-image.nix +41/−0
- src/nix/modules/composition/docker-compose.nix +78/−0
- src/nix/modules/composition/host-environment.nix +33/−0
- src/nix/modules/composition/images.nix +39/−0
- src/nix/modules/composition/service-info.nix +21/−0
- src/nix/modules/lib/assert.nix +32/−0
- src/nix/modules/lib/assertions.nix +34/−0
- src/nix/modules/nixos/container-systemd.nix +24/−0
- src/nix/modules/nixos/default-shell.nix +4/−0
- src/nix/modules/service/all-modules.nix +11/−0
- src/nix/modules/service/context.nix +19/−0
- src/nix/modules/service/default-exec.nix +19/−0
- src/nix/modules/service/docker-compose-service.nix +273/−0
- src/nix/modules/service/extended-info.nix +23/−0
- src/nix/modules/service/host-store.nix +41/−0
- src/nix/modules/service/image.nix +122/−0
- src/nix/modules/service/nixos-init.nix +42/−0
- src/nix/modules/service/nixos.nix +60/−0
+ 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 <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><name></code> in the composition-level <code>services.<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.<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;+ };+}