├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ci ├── jobset.nix ├── jobsets-declarative.nix └── spec.json ├── default.nix ├── docs ├── default.nix ├── misc.md └── options-well-supported-generated.md ├── images ├── example-systemd.nix ├── example.nix └── nix.nix ├── lib ├── default.nix ├── documentation.nix └── make-image.nix ├── modules ├── fake.nix ├── image.nix ├── nix-daemon.nix ├── s6-lib.nix ├── s6.nix ├── system-path.nix ├── system.nix └── systemd.nix ├── nix ├── sources.json └── sources.nix ├── overlay.nix └── tests ├── env.nix ├── exposed-ports.nix ├── from.nix ├── minimal-image-size.nix ├── nginx.nix ├── nix.nix ├── readme.nix ├── s6-image.nix ├── s6.nix └── systemd.nix /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | result 3 | result-* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Orange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docs: 2 | nix-build default.nix \ 3 | --attr docs.optionsMarkdownWellSuppored \ 4 | --out-link ./options-well-supported-generated.md 5 | cp --no-preserve=mode \ 6 | options-well-supported-generated.md \ 7 | docs/options-well-supported-generated.md 8 | 9 | clean: 10 | rm -f ./options-well-supported-generated.md 11 | 12 | .PHONY: clean docs 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Declarative container images with Nix 2 | 3 | **warning: this project is no longer maintained** 4 | 5 | With this project you can 6 | - make your image composable (thanks to the NixOS modules system) 7 | - integrate the [s6](https://www.skarnet.org/software/s6/) init system in your images 8 | - reuse some NixOS modules in a container... without relying on systemd 9 | - make a Nix a Docker image, built by Nix 10 | 11 | 12 | ## Getting started 13 | 14 | To build a Docker image named `hello` that runs `hello`. 15 | 16 | ```nix 17 | lib.makeImage ( 18 | { pkgs, ... }: { 19 | config.image = { 20 | name = "hello"; 21 | entryPoint = [ "${pkgs.hello}/bin/hello" ]; 22 | }; 23 | }) 24 | ``` 25 | 26 | - To build an empty image from CLI, 27 | ``` 28 | nix-build -E 'with import ./default.nix{}; lib.makeImage{ config.image.name = "empty"; }' 29 | ``` 30 | 31 | - To use `lib.makeImage` in your project, add `overlay.nix` to your 32 | [nixpkgs overlay list](https://nixos.org/nixpkgs/manual/#sec-overlays-install). 33 | 34 | The [`image`](#module-image) module section for more information. 35 | 36 | 37 | ## Use s6 as init system to run services 38 | 39 | The [s6 module](#module-s6) can be used to build an image with an init 40 | system. The [s6 init system](https://www.skarnet.org/software/s6/) is 41 | used to run defined `s6.services`. 42 | 43 | ```nix 44 | lib.makeImage ({ pkgs, ... }: { 45 | config = { 46 | image.name = "s6"; 47 | s6.services.nginx = { 48 | execStart = ''${pkgs.nginx}/bin/nginx -g "daemon off;"''; 49 | }; 50 | }; 51 | }) 52 | ``` 53 | 54 | Some goals of using an init system in a container are 55 | - Proper PID 1 (no zombie processes) 56 | - Run several services in one container 57 | - Processes debugging (if the process is killed or died, the container 58 | is not necessarily killed) 59 | - Execute initialization tasks 60 | 61 | 62 | See [s6 module](#module-s6) for details. 63 | 64 | 65 | ## (Re)Use NixOS modules 66 | 67 | Some NixOS modules can be used, such as `users`, `etc`. 68 | 69 | ```nix 70 | lib.makeImage ({ pkgs, ... }: { 71 | config = { 72 | image.name = "nixos"; 73 | environment.systemPackages = [ pkgs.coreutils ]; 74 | users.users.alice = { 75 | isNormalUser = true; 76 | }; 77 | }; 78 | }) 79 | ``` 80 | 81 | See also [supported NixOS modules](#supported-nixos-modules). 82 | 83 | 84 | ## Systemd support for NixOS modules :/ 85 | 86 | It is possible to run some NixOS modules defining systemd services 87 | thanks to a partial systemd implementation with s6. 88 | 89 | Note this implementation is fragile, experimental and partial! 90 | 91 | ```nix 92 | lib.makeImage ({ pkgs, ... }: { 93 | config = { 94 | image.name = "nginx"; 95 | # Yeah! It is the NixOS module! 96 | services.nginx.enable = true; 97 | }; 98 | }) 99 | ``` 100 | 101 | 102 | ## Predefined images 103 | 104 | - [nix](images/nix.nix): basic single user installation 105 | 106 | These images can be built with `nix-build -A dockerImages`. 107 | 108 | More configurations and images are also available in the 109 | [tests directory](./tests). 110 | 111 | 112 | ## Module `image` 113 | 114 | The `image` module defines common Docker image attributes, such as the 115 | image name, the environment variables, etc. Please refer to 116 | [the `image` options documentation](docs/options-well-supported-generated.md#imageentrypoint). 117 | 118 | 119 | ## Module `s6` 120 | 121 | This module allows you to easily create services, managed by the 122 | [s6 init system](https://www.skarnet.org/software/s6/). Three types of 123 | services can be defined: 124 | 125 | - `oneshot-pre` services are exectued sequentially at container start 126 | time and must terminate. They can be ordered thanks to the `after` 127 | option. 128 | - `long-run` services are for daemons and are managed by `s6`. There 129 | is no notion of dependency for `long-run` services. 130 | - `oneshot-post` services are executed sequentially once all `long-run` 131 | services have been started. They can also be ordered (`after` 132 | option). They are generally used to provision started services. 133 | 134 | Options are described in this 135 | [generated `s6` options documentation](docs/options-well-supported-generated.md#s6services). 136 | 137 | 138 | ### How/when s6 main process is terminated 139 | 140 | By default, if a s6 service fails, the `s6-svcscan` (PID 1 in a 141 | container) process is terminated. A `long-run` service can set the 142 | `restartOnFailure` option to `true` to restart the service when it 143 | fails. 144 | 145 | If the `S6_DONT_TERMINATE_ON_ERROR` environment variable is set, 146 | `s6-svscan` is not terminated on service failure. This can be used to 147 | debug a failing service interactively. 148 | 149 | 150 | ## Supported NixOS modules 151 | 152 | - `users`: create users and groups 153 | - `nix`: configure Nix 154 | - `environment.etc`: create files in `/etc` 155 | - `systemd`: a small subset of the systemd module is implemented with [s6](https://www.skarnet.org/software/s6/) 156 | - `nginx`: see its [test](./tests/nginx.nix) 157 | 158 | Important: only a small subset of NixOS modules is supported. See the 159 | [tests directory](./tests) for supported (and tested) features. 160 | 161 | 162 | ## Tests 163 | 164 | - [s6](tests/s6.nix): s6 tests executed in the Nix build environment (fast to run but limited) 165 | - [dockerImages](tests/): tests on Docker images executed in a NixOS VM. 166 | 167 | 168 | ## Implementation of the NixOS systemd service interface 169 | 170 | A subset of the NixOS systemd services interface is supported and 171 | implemented with the [s6](https://www.skarnet.org/software/s6/) init 172 | system. 173 | 174 | There are several differences with the NixOS systemd 175 | implementation. The main one is the service dependency model: 176 | 177 | - Services of type `simple` become `long-run` s6 services and dependencies are ignored. 178 | - Services of type `oneshot` become `onehost-pre` s6 services except 179 | if they have an `after` dependency to a `simple` service. In this 180 | case, they become `oneshot-post`. Dependencies between oneshot 181 | services are respected. 182 | 183 | 184 | ## Tips 185 | 186 | - To generate and run the container init script as user 187 | ``` 188 | nix-build -A dockerImages.example-systemd.init 189 | ./result S6-STATE-DIR 190 | ``` 191 | - To get the image used by a test `nix-build -A tests.dockerImages.nginx.image` 192 | - The NixOS config of an image `nix-instantiate --eval -A tests.dockerImages.nginx.image.config` 193 | - The NixOS config of an s6 test `nix-instantiate --eval -A tests.s6.path.config.systemd` 194 | 195 | 196 | ## Related projects 197 | 198 | - [s6-overlay](https://github.com/just-containers/s6-overlay) 199 | - [nix-docker-nix](https://github.com/garbas/nix-docker-nix) 200 | 201 | 202 | ## Contributing 203 | 204 | Contributions to nix-container-images through PRs are always 205 | welcome. All PRs will be automatically tested by the [Hydra CI 206 | server](https://hydra.nix.corp.cloudwatt.com/project/nix-container-images). 207 | -------------------------------------------------------------------------------- /ci/jobset.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = (import ../nix/sources.nix).nixpkgs; 3 | pkgs = import nixpkgs { 4 | overlays = [ (import ../overlay.nix) ]; 5 | }; 6 | in 7 | { 8 | inherit (pkgs) dockerImages; 9 | tests.nixContainerImages = pkgs.tests.nixContainerImages; 10 | } 11 | -------------------------------------------------------------------------------- /ci/jobsets-declarative.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs, declInput, pulls }: 2 | let 3 | pkgs = import nixpkgs {}; 4 | prs = builtins.fromJSON (builtins.readFile pulls); 5 | prJobsets = pkgs.lib.mapAttrs (num: info: 6 | { enabled = 1; 7 | hidden = false; 8 | description = "PR ${num}: ${info.title}"; 9 | nixexprinput = "nixexpr"; 10 | nixexprpath = "ci/jobset.nix"; 11 | checkinterval = 60; 12 | schedulingshares = 20; 13 | enableemail = false; 14 | emailoverride = ""; 15 | keepnr = 1; 16 | inputs = { 17 | nixexpr = { 18 | type = "git"; 19 | value = "git://github.com/${info.base.repo.owner.login}/${info.base.repo.name}.git pull/${num}/head"; 20 | emailresponsible = false; 21 | }; 22 | nixpkgs = { 23 | value = "https://github.com/NixOS/nixpkgs f52505fac8c82716872a616c501ad9eff188f97f"; 24 | type = "git"; 25 | emailresponsible = false; 26 | }; 27 | }; 28 | } 29 | ) prs; 30 | desc = prJobsets // { 31 | trunk = { 32 | description = "Build master of cloudwatt/nix-container-images"; 33 | checkinterval = 60; 34 | enabled = 1; 35 | nixexprinput = "nixexpr"; 36 | nixexprpath = "ci/jobset.nix"; 37 | schedulingshares = 100; 38 | enableemail = false; 39 | emailoverride = ""; 40 | keepnr = 3; 41 | hidden = false; 42 | inputs = { 43 | nixexpr = { 44 | value = "https://github.com/cloudwatt/nix-container-images"; 45 | type = "git"; 46 | emailresponsible = false; 47 | }; 48 | # Only used by Niv to get its fetchers. 49 | # Belongs to branch 19.03. 50 | nixpkgs = { 51 | value = "https://github.com/NixOS/nixpkgs f52505fac8c82716872a616c501ad9eff188f97f"; 52 | type = "git"; 53 | emailresponsible = false; 54 | }; 55 | }; 56 | }; 57 | }; 58 | 59 | in { 60 | jobsets = pkgs.runCommand "spec.json" {} '' 61 | cat >$out <.cryptHomeLuks" ]; 10 | 11 | in 12 | { 13 | optionsJsonWellSupported = lib.optionsToJson "options-well-supported" wellSupported; 14 | optionsMarkdownWellSuppored = lib.optionsToMarkdown "options-well-supported" wellSupported; 15 | } 16 | -------------------------------------------------------------------------------- /docs/misc.md: -------------------------------------------------------------------------------- 1 | ## Todos / Limitations 2 | 3 | - systemd implementation is fragile 4 | - Do not have to patch `nix-daemon.nix` 5 | - Do not have to patch `update-users-groups.pl` 6 | - Warnings on non implemented systemd features 7 | 8 | 9 | ## What could be improved in nixpkgs 10 | 11 | - The base directory of NixOS activation script should be configurable. It is currently `/` 12 | - Split service runtime and installation/build time in modules: it's hard to 13 | only use the part of a module that generate the configuration file 14 | of a service. At evaluation, a lot of modules need to be imported 15 | while they are only used by the service runtime part. 16 | - Explicit dependencies between modules 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/options-well-supported-generated.md: -------------------------------------------------------------------------------- 1 | #### environment.etc 2 | 3 | Set of files that have to be linked in /etc. 4 | 5 | 6 | - type: `list or attribute set of submodules` 7 | - default: `{}` 8 | 9 | 10 | #### environment.etc..enable 11 | 12 | Whether this /etc file should be generated. This 13 | option allows specific /etc files to be disabled. 14 | 15 | 16 | - type: `boolean` 17 | - default: `true` 18 | 19 | 20 | #### environment.etc..gid 21 | 22 | GID of created file. Only takes affect when the file is 23 | copied (that is, the mode is not 'symlink'). 24 | 25 | 26 | - type: `signed integer` 27 | - default: `0` 28 | 29 | 30 | #### environment.etc..group 31 | 32 | Group name of created file. 33 | Only takes affect when the file is copied (that is, the mode is not 'symlink'). 34 | Changing this option takes precedence over gid. 35 | 36 | 37 | - type: `string` 38 | - default: `+0` 39 | 40 | 41 | #### environment.etc..mode 42 | 43 | If set to something else than symlink, 44 | the file is copied instead of symlinked, with the given 45 | file mode. 46 | 47 | 48 | - type: `string` 49 | - default: `symlink` 50 | 51 | 52 | #### environment.etc..source 53 | 54 | Path of the source file. 55 | 56 | - type: `path` 57 | - default: `null` 58 | 59 | 60 | #### environment.etc..target 61 | 62 | Name of symlink (relative to 63 | /etc). Defaults to the attribute 64 | name. 65 | 66 | 67 | - type: `string` 68 | - default: `null` 69 | 70 | 71 | #### environment.etc..text 72 | 73 | Text of the file. 74 | 75 | - type: `null or strings concatenated with "\n"` 76 | - default: `null` 77 | 78 | 79 | #### environment.etc..uid 80 | 81 | UID of created file. Only takes affect when the file is 82 | copied (that is, the mode is not 'symlink'). 83 | 84 | 85 | - type: `signed integer` 86 | - default: `0` 87 | 88 | 89 | #### environment.etc..user 90 | 91 | User name of created file. 92 | Only takes affect when the file is copied (that is, the mode is not 'symlink'). 93 | Changing this option takes precedence over uid. 94 | 95 | 96 | - type: `string` 97 | - default: `+0` 98 | 99 | 100 | #### environment.systemPackages 101 | 102 | The set of packages that appear in 103 | /run/current-system/sw. These packages are 104 | automatically available to all users, and are 105 | automatically updated every time you rebuild the system 106 | configuration. (The latter is the main difference with 107 | installing them in the default profile, 108 | /nix/var/nix/profiles/default. 109 | 110 | 111 | - type: `list of packages` 112 | - default: `[]` 113 | 114 | 115 | #### image.entryPoint 116 | 117 | Entry point command list 118 | 119 | 120 | - type: `list of strings` 121 | - default: `[]` 122 | 123 | 124 | #### image.env 125 | 126 | Environment variables 127 | 128 | 129 | - type: `attribute set` 130 | - default: `{}` 131 | 132 | 133 | #### image.from 134 | 135 | The parent image 136 | 137 | 138 | - type: `null or package` 139 | - default: `null` 140 | 141 | 142 | #### image.interactive 143 | 144 | Add packages for an interactive use of the container 145 | (bashInteractive, coreutils) 146 | 147 | 148 | - type: `boolean` 149 | - default: `false` 150 | 151 | 152 | #### image.name 153 | 154 | The name of the image 155 | 156 | 157 | - type: `string` 158 | - default: `null` 159 | 160 | 161 | #### image.run 162 | 163 | Extra commands run at container build time 164 | 165 | 166 | - type: `strings concatenated with "\n"` 167 | - default: `` 168 | 169 | 170 | #### image.tag 171 | 172 | The tag of the image 173 | 174 | 175 | - type: `null or string` 176 | - default: `null` 177 | 178 | 179 | #### s6.init 180 | 181 | The generated init script. 182 | 183 | - type: `null or package` 184 | - default: `null` 185 | 186 | 187 | #### s6.services 188 | 189 | Definition of s6 service. 190 | 191 | - type: `attribute set of submodules` 192 | - default: `{}` 193 | 194 | 195 | #### s6.services..after 196 | 197 | Configure ordering dependencies between units. 198 | 199 | - type: `list of strings` 200 | - default: `[]` 201 | 202 | 203 | #### s6.services..enable 204 | 205 | Whether to enable the service 206 | 207 | - type: `boolean` 208 | - default: `true` 209 | 210 | 211 | #### s6.services..environment 212 | 213 | Environment variables passed to the service's processes. 214 | 215 | - type: `attribute set of null or string or path or packages` 216 | - default: `{}` 217 | 218 | 219 | #### s6.services..execLogger 220 | 221 | Command executed as the service's logger: it gets the stdout of the main process. 222 | 223 | - type: `null or string or package` 224 | - default: `null` 225 | 226 | 227 | #### s6.services..execStart 228 | 229 | Command executed as the service's main process. 230 | 231 | - type: `string or package` 232 | - default: `` 233 | 234 | 235 | #### s6.services..restartOnFailure 236 | 237 | Restart the service if it fails. Note this is only used by long-run services. 238 | 239 | - type: `boolean` 240 | - default: `false` 241 | 242 | 243 | #### s6.services..script 244 | 245 | Shell commands executed as the service's main process. 246 | 247 | - type: `strings concatenated with "\n"` 248 | - default: `` 249 | 250 | 251 | #### s6.services..type 252 | 253 | Type of the s6 service (oneshot-pre, long-run or oneshot-post). 254 | 255 | - type: `one of "long-run", "oneshot-pre", "oneshot-post"` 256 | - default: `long-run` 257 | 258 | 259 | #### s6.services..user 260 | 261 | Set the UNIX user that the processes are executed as. 262 | 263 | - type: `string` 264 | - default: `root` 265 | 266 | 267 | #### s6.services..workingDirectory 268 | 269 | Sets the working directory for executed processes. 270 | 271 | - type: `null or string` 272 | - default: `null` 273 | 274 | 275 | #### users.defaultUserShell 276 | 277 | This option defines the default shell assigned to user 278 | accounts. This can be either a full system path or a shell package. 279 | 280 | This must not be a store path, since the path is 281 | used outside the store (in particular in /etc/passwd). 282 | 283 | 284 | - type: `path or package` 285 | - default: `null` 286 | 287 | 288 | #### users.enforceIdUniqueness 289 | 290 | Whether to require that no two users/groups share the same uid/gid. 291 | 292 | 293 | - type: `boolean` 294 | - default: `true` 295 | 296 | 297 | #### users.groups 298 | 299 | Additional groups to be created automatically by the system. 300 | 301 | 302 | - type: `list or attribute set of submodules` 303 | - default: `{}` 304 | 305 | 306 | #### users.groups..gid 307 | 308 | The group GID. If the GID is null, a free GID is picked on 309 | activation. 310 | 311 | 312 | - type: `null or signed integer` 313 | - default: `null` 314 | 315 | 316 | #### users.groups..members 317 | 318 | The user names of the group members, added to the 319 | /etc/group file. 320 | 321 | 322 | - type: `list of Concatenated strings` 323 | - default: `[]` 324 | 325 | 326 | #### users.groups..name 327 | 328 | The name of the group. If undefined, the name of the attribute set 329 | will be used. 330 | 331 | 332 | - type: `string` 333 | - default: `null` 334 | 335 | 336 | #### users.mutableUsers 337 | 338 | If set to true, you are free to add new users and groups to the system 339 | with the ordinary useradd and 340 | groupadd commands. On system activation, the 341 | existing contents of the /etc/passwd and 342 | /etc/group files will be merged with the 343 | contents generated from the users.users and 344 | users.groups options. 345 | The initial password for a user will be set 346 | according to users.users, but existing passwords 347 | will not be changed. 348 | 349 | 350 | If set to false, the contents of the user and 351 | group files will simply be replaced on system activation. This also 352 | holds for the user passwords; all changed 353 | passwords will be reset according to the 354 | users.users configuration on activation. 355 | 356 | 357 | 358 | - type: `boolean` 359 | - default: `true` 360 | 361 | 362 | #### users.users 363 | 364 | Additional user accounts to be created automatically by the system. 365 | This can also be used to set options for root. 366 | 367 | 368 | - type: `list or attribute set of submodules` 369 | - default: `{}` 370 | 371 | 372 | #### users.users..createHome 373 | 374 | If true, the home directory will be created automatically. If this 375 | option is true and the home directory already exists but is not 376 | owned by the user, directory owner and group will be changed to 377 | match the user. 378 | 379 | 380 | - type: `boolean` 381 | - default: `false` 382 | 383 | 384 | #### users.users..description 385 | 386 | A short description of the user account, typically the 387 | user's full name. This is actually the “GECOS” or “comment” 388 | field in /etc/passwd. 389 | 390 | 391 | - type: `string` 392 | - default: `` 393 | 394 | 395 | #### users.users..extraGroups 396 | 397 | The user's auxiliary groups. 398 | 399 | - type: `list of strings` 400 | - default: `[]` 401 | 402 | 403 | #### users.users..group 404 | 405 | The user's primary group. 406 | 407 | - type: `string` 408 | - default: `nogroup` 409 | 410 | 411 | #### users.users..hashedPassword 412 | 413 | Specifies the hashed password for the user. 414 | The options , 415 | and 416 | controls what password is set for the user. 417 | overrides both 418 | and . 419 | overrides . 420 | If none of these three options are set, no password is assigned to 421 | the user, and the user will not be able to do password logins. 422 | If the option is true, the 423 | password defined in one of the three options will only be set when 424 | the user is created for the first time. After that, you are free to 425 | change the password with the ordinary user management commands. If 426 | is false, you cannot change 427 | user passwords, they will always be set according to the password 428 | options. 429 | 430 | To generate hashed password install mkpasswd 431 | package and run mkpasswd -m sha-512. 432 | 433 | 434 | 435 | - type: `null or string` 436 | - default: `null` 437 | 438 | 439 | #### users.users..home 440 | 441 | The user's home directory. 442 | 443 | - type: `path` 444 | - default: `/var/empty` 445 | 446 | 447 | #### users.users..initialHashedPassword 448 | 449 | Specifies the initial hashed password for the user, i.e. the 450 | hashed password assigned if the user does not already 451 | exist. If is true, the 452 | password can be changed subsequently using the 453 | passwd command. Otherwise, it's 454 | equivalent to setting the option. 455 | 456 | To generate hashed password install mkpasswd 457 | package and run mkpasswd -m sha-512. 458 | 459 | 460 | 461 | - type: `null or string` 462 | - default: `null` 463 | 464 | 465 | #### users.users..initialPassword 466 | 467 | Specifies the initial password for the user, i.e. the 468 | password assigned if the user does not already exist. If 469 | is true, the password 470 | can be changed subsequently using the 471 | passwd command. Otherwise, it's 472 | equivalent to setting the 473 | option. The same caveat applies: the password specified here 474 | is world-readable in the Nix store, so it should only be 475 | used for guest accounts or passwords that will be changed 476 | promptly. 477 | 478 | 479 | - type: `null or string` 480 | - default: `null` 481 | 482 | 483 | #### users.users..isNormalUser 484 | 485 | Indicates whether this is an account for a “real” user. This 486 | automatically sets to 487 | users, to 488 | true, to 489 | /home/username, 490 | to true, 491 | and to 492 | false. 493 | 494 | 495 | - type: `boolean` 496 | - default: `false` 497 | 498 | 499 | #### users.users..isSystemUser 500 | 501 | Indicates if the user is a system user or not. This option 502 | only has an effect if is 503 | , in which case it determines whether 504 | the user's UID is allocated in the range for system users 505 | (below 500) or in the range for normal users (starting at 506 | 1000). 507 | 508 | 509 | - type: `boolean` 510 | - default: `false` 511 | 512 | 513 | #### users.users..name 514 | 515 | The name of the user account. If undefined, the name of the 516 | attribute set will be used. 517 | 518 | 519 | - type: `string` 520 | - default: `null` 521 | 522 | 523 | #### users.users..packages 524 | 525 | The set of packages that should be made availabe to the user. 526 | This is in contrast to , 527 | which adds packages to all users. 528 | 529 | 530 | - type: `list of packages` 531 | - default: `[]` 532 | 533 | 534 | #### users.users..password 535 | 536 | Specifies the (clear text) password for the user. 537 | Warning: do not set confidential information here 538 | because it is world-readable in the Nix store. This option 539 | should only be used for public accounts. 540 | The options , 541 | and 542 | controls what password is set for the user. 543 | overrides both 544 | and . 545 | overrides . 546 | If none of these three options are set, no password is assigned to 547 | the user, and the user will not be able to do password logins. 548 | If the option is true, the 549 | password defined in one of the three options will only be set when 550 | the user is created for the first time. After that, you are free to 551 | change the password with the ordinary user management commands. If 552 | is false, you cannot change 553 | user passwords, they will always be set according to the password 554 | options. 555 | 556 | 557 | 558 | - type: `null or string` 559 | - default: `null` 560 | 561 | 562 | #### users.users..passwordFile 563 | 564 | The full path to a file that contains the user's password. The password 565 | file is read on each system activation. The file should contain 566 | exactly one line, which should be the password in an encrypted form 567 | that is suitable for the chpasswd -e command. 568 | The options , 569 | and 570 | controls what password is set for the user. 571 | overrides both 572 | and . 573 | overrides . 574 | If none of these three options are set, no password is assigned to 575 | the user, and the user will not be able to do password logins. 576 | If the option is true, the 577 | password defined in one of the three options will only be set when 578 | the user is created for the first time. After that, you are free to 579 | change the password with the ordinary user management commands. If 580 | is false, you cannot change 581 | user passwords, they will always be set according to the password 582 | options. 583 | 584 | 585 | 586 | - type: `null or Concatenated string` 587 | - default: `null` 588 | 589 | 590 | #### users.users..shell 591 | 592 | The path to the user's shell. Can use shell derivations, 593 | like pkgs.bashInteractive. Don’t 594 | forget to enable your shell in 595 | programs if necessary, 596 | like programs.zsh.enable = true;. 597 | 598 | 599 | - type: `package or path` 600 | - default: `pkgs.shadow` 601 | 602 | 603 | #### users.users..subGidRanges 604 | 605 | Subordinate group ids that user is allowed to use. 606 | They are set into /etc/subgid and are used 607 | by newgidmap for user namespaces. 608 | 609 | 610 | - type: `list of submodules` 611 | - default: `[]` 612 | 613 | 614 | #### users.users..subGidRanges.*.count 615 | 616 | Count of subordinate group ids 617 | 618 | - type: `signed integer` 619 | - default: `1` 620 | 621 | 622 | #### users.users..subGidRanges.*.startGid 623 | 624 | Start of the range of subordinate group ids that user is 625 | allowed to use. 626 | 627 | 628 | - type: `signed integer` 629 | - default: `null` 630 | 631 | 632 | #### users.users..subUidRanges 633 | 634 | Subordinate user ids that user is allowed to use. 635 | They are set into /etc/subuid and are used 636 | by newuidmap for user namespaces. 637 | 638 | 639 | - type: `list of submodules` 640 | - default: `[]` 641 | 642 | 643 | #### users.users..subUidRanges.*.count 644 | 645 | Count of subordinate user ids 646 | 647 | - type: `signed integer` 648 | - default: `1` 649 | 650 | 651 | #### users.users..subUidRanges.*.startUid 652 | 653 | Start of the range of subordinate user ids that user is 654 | allowed to use. 655 | 656 | 657 | - type: `signed integer` 658 | - default: `null` 659 | 660 | 661 | #### users.users..uid 662 | 663 | The account UID. If the UID is null, a free UID is picked on 664 | activation. 665 | 666 | 667 | - type: `null or signed integer` 668 | - default: `null` 669 | 670 | 671 | #### users.users..useDefaultShell 672 | 673 | If true, the user's shell will be set to 674 | . 675 | 676 | 677 | - type: `boolean` 678 | - default: `false` 679 | 680 | 681 | -------------------------------------------------------------------------------- /images/example-systemd.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ...}: 2 | 3 | { 4 | config = { 5 | image = { 6 | name = "systemd"; 7 | }; 8 | 9 | systemd.services.script = { 10 | environment = { OUTPUT = "output"; }; 11 | script = '' 12 | while true; do 13 | echo systemd.services.daemon.script with environment.OUTPUT=$OUTPUT 14 | sleep 1 15 | done 16 | ''; 17 | }; 18 | 19 | systemd.services.oneshot = { 20 | script = '' 21 | echo systemd.services.oneshot 22 | ''; 23 | serviceConfig.Type = "oneshot"; 24 | }; 25 | 26 | systemd.services.execStart = { 27 | serviceConfig.ExecStart = ''${pkgs.bash}/bin/bash -c "echo serviceConfig.ExecStart"''; 28 | }; 29 | 30 | systemd.services.preStart = { 31 | preStart = '' 32 | echo systemd.services.pre-start.preStart 33 | ''; 34 | script = '' 35 | while true; do 36 | echo systemd.services.pre-start.script 37 | sleep 1 38 | done 39 | ''; 40 | }; 41 | 42 | systemd.services.first = { 43 | serviceConfig.Type = "oneshot"; 44 | script = '' 45 | echo systemd.services.first begin 46 | sleep 3 47 | echo dependent services 1 system.services.first 48 | ''; 49 | }; 50 | 51 | systemd.services.second = { 52 | requires = [ "first.service" ]; 53 | after = [ "first.service" ]; 54 | script = '' 55 | echo dependent services 2 system.services.second 56 | ''; 57 | }; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /images/example.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ...}: 2 | 3 | { 4 | config = { 5 | 6 | image = { 7 | name = "hello"; 8 | tag = "latest"; 9 | }; 10 | 11 | environment.systemPackages = [ pkgs.hello ]; 12 | 13 | users.users.alice = { 14 | isNormalUser = true; 15 | home = "/home/alice"; 16 | description = "Alice Foobar"; 17 | extraGroups = [ "wheel" ]; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /images/nix.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ...}: 2 | 3 | { 4 | imports = [ ]; 5 | config = { 6 | image = { 7 | name = "nix"; 8 | run = '' 9 | chmod u+w root 10 | echo 'https://nixos.org/channels/19.03 nixpkgs' > root/.nix-channels 11 | ''; 12 | interactive = true; 13 | }; 14 | 15 | environment.systemPackages = [ pkgs.nix ]; 16 | 17 | nix = { 18 | enable = true; 19 | useSandbox = false; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | with builtins; 4 | with import (pkgs.path + /nixos/lib/testing.nix) { system = builtins.currentSystem; }; 5 | 6 | let 7 | documentation = pkgs.callPackages ./documentation.nix {}; 8 | in 9 | rec { 10 | inherit (documentation) filterOptions optionsToJson optionsToMarkdown; 11 | 12 | # Takes the image derivation and returns the hash 13 | imageHash = image: head (split "-" (baseNameOf image)); 14 | imageRef = image: "${image.imageName}:${imageHash image}"; 15 | 16 | makeImage = pkgs.callPackage ./make-image.nix {}; 17 | 18 | # Build a test vm with Docker enable 19 | # It also exposes the `image` attribute. 20 | makeContainerTest = { image, testScript }: 21 | let 22 | machine = { config, ... }: { 23 | config = { 24 | virtualisation = { 25 | docker.enable = true; 26 | diskSize = 1024; 27 | }; 28 | }; 29 | }; 30 | in 31 | makeTest { 32 | name = image.imageName; 33 | nodes = { inherit machine; }; 34 | inherit testScript; 35 | } // 36 | { inherit image; }; 37 | } 38 | -------------------------------------------------------------------------------- /lib/documentation.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | let 4 | # Remove invisible and internal options. 5 | optionsListVisible = options: pkgs.lib.filter (opt: opt.visible && !opt.internal) (pkgs.lib.optionAttrSetToDocList options); 6 | in 7 | 8 | rec { 9 | filterOptions = options: supportedPrefixes: unsupportedPrefixes: let 10 | supported = opt: pkgs.lib.any (p: pkgs.lib.hasPrefix p opt.name) supportedPrefixes; 11 | unsupported = opt: ! (pkgs.lib.any (p: pkgs.lib.hasPrefix p opt.name) unsupportedPrefixes); 12 | in 13 | pkgs.lib.filter unsupported (pkgs.lib.filter supported (optionsListVisible options)); 14 | 15 | optionsToJson = name: options: pkgs.writeTextFile { 16 | name = "${name}.json"; 17 | text = (builtins.toJSON options); 18 | }; 19 | 20 | optionsToMarkdown = name: options: 21 | pkgs.runCommand "${name}.md" { buildInputs = [ pkgs.jq ]; } '' 22 | cat ${optionsToJson "${name}.json" options} | jq '.[] | "#### \(.name)\n\n\(.description)\n\n- type: `\(.type)`\n- default: `\(.default)`\n\n"' -r > $out 23 | ''; 24 | } 25 | -------------------------------------------------------------------------------- /lib/make-image.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | 3 | with pkgs.lib; 4 | 5 | module: 6 | 7 | let 8 | eval = evalModules { 9 | modules = [ module ] ++ [ 10 | # To not have to import all NixOS modules... 11 | ../modules/fake.nix 12 | ../modules/system.nix 13 | ../modules/image.nix 14 | # This is to make nix optionnal 15 | ../modules/nix-daemon.nix 16 | ../modules/s6.nix 17 | ../modules/systemd.nix 18 | ../modules/system-path.nix 19 | ] ++ (map (m: (pkgs.path + "/nixos/modules/") + m) [ 20 | "/system/etc/etc.nix" 21 | "/config/users-groups.nix" 22 | "/misc/assertions.nix" 23 | "/config/shells-environment.nix" 24 | "/config/system-environment.nix" 25 | "/programs/environment.nix" 26 | "/misc/ids.nix" 27 | "/programs/bash/bash.nix" 28 | "/security/pam.nix" 29 | "/security/wrappers/default.nix" 30 | "/programs/shadow.nix" 31 | "/security/ca.nix" 32 | "/misc/meta.nix" 33 | "/misc/version.nix" 34 | "/services/continuous-integration/hydra/default.nix" 35 | "/services/databases/postgresql.nix" 36 | "/services/web-servers/nginx/default.nix" 37 | ]); 38 | args = { 39 | inherit pkgs lib; 40 | utils = import (pkgs.path + /nixos/lib/utils.nix) pkgs; 41 | }; 42 | }; 43 | 44 | 45 | doc = import (pkgs.path + "../../doc/manual") { 46 | inherit pkgs; 47 | config = ({...}:{}); 48 | version = ""; 49 | revision = ""; 50 | options = eval.options; 51 | }; 52 | 53 | # Activation user script is patched because it is creating 54 | # files in `/` while they have to be created in the build directory. 55 | activationScriptUsers = let 56 | userSpec = pkgs.lib.last (pkgs.lib.splitString " " eval.config.system.activationScripts.users.text); 57 | updateUsersGroupsPatched = pkgs.runCommand 58 | "update-users-groups-patched" 59 | { buildInputs = [ pkgs.gnused ]; } 60 | '' 61 | sed 's|/etc|etc|g;s|/var|var|g;s|nscd|true|g' ${(pkgs.path + /nixos/modules/config/update-users-groups.pl)} > $out 62 | ''; 63 | in 64 | pkgs.runCommand "passwd-groups" { inherit userSpec; buildInputs = [ pkgs.jq ];} '' 65 | mkdir system 66 | cd system 67 | 68 | mkdir -p etc root $out 69 | 70 | # home dirs have to be created in the build directory 71 | sed 's|/home|home|g;s|/var|var|g' $userSpec > ../userSpecPatched 72 | 73 | ${pkgs.perl}/bin/perl -w \ 74 | -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl \ 75 | -I${pkgs.perlPackages.JSON}/lib/perl5/site_perl \ 76 | ${updateUsersGroupsPatched} ../userSpecPatched 77 | 78 | cp -r * $out/ 79 | ''; 80 | 81 | # This comes from 82 | failedAssertions = map (x: x.message) (filter (x: !x.assertion) eval.config.assertions); 83 | showWarnings = res: fold (w: x: builtins.trace "warning: ${w}" x) res eval.config.warnings; 84 | withAssertions = f: if failedAssertions != [] 85 | then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}" 86 | else showWarnings f; 87 | 88 | containerBuilder = 89 | if eval.config.nix.enable 90 | then pkgs.dockerTools.buildImageWithNixDb 91 | else pkgs.dockerTools.buildImage; 92 | 93 | in withAssertions (containerBuilder { 94 | name = eval.config.image.name; 95 | tag = eval.config.image.tag; 96 | fromImage = eval.config.image.from; 97 | contents = [ 98 | activationScriptUsers 99 | eval.config.system.path 100 | eval.config.system.build.etc ]; 101 | extraCommands = eval.config.image.run; 102 | config = { 103 | EntryPoint = eval.config.image.entryPoint; 104 | Env = mapAttrsToList (n: v: "${n}=${v}") eval.config.image.env; 105 | ExposedPorts = eval.config.image.exposedPorts; 106 | }; 107 | }) 108 | // 109 | # For debugging purposes 110 | { 111 | config = eval.config; 112 | options = eval.options; 113 | } 114 | // 115 | (optionalAttrs 116 | (eval.config.s6.init != null) 117 | { inherit (eval.config.s6) init; }) 118 | -------------------------------------------------------------------------------- /modules/fake.nix: -------------------------------------------------------------------------------- 1 | # We add fake options because we don't want to import all NixOS 2 | # modules. 3 | 4 | { config, pkgs, lib, ...}: 5 | 6 | with lib; 7 | 8 | { 9 | options = { 10 | 11 | services.xserver.displayManager.hiddenUsers = mkOption { 12 | type = types.listOf types.str; 13 | default = [ "nobody" ]; 14 | description = '' 15 | A list of users which will not be shown in the display manager. 16 | ''; 17 | }; 18 | 19 | 20 | system.activationScripts = mkOption { 21 | visible = false; 22 | default = {}; 23 | }; 24 | 25 | services.sssd.enable = mkOption { 26 | visible = false; 27 | type = types.bool; 28 | default = false; 29 | }; 30 | krb5.enable = mkOption { 31 | visible = false; 32 | type = types.bool; 33 | default = false; 34 | }; 35 | services.fprintd.enable = mkOption { 36 | visible = false; 37 | type = types.bool; 38 | default = false; 39 | }; 40 | security.pam.usb.enable = mkOption { 41 | visible = false; 42 | type = types.bool; 43 | default = false; 44 | }; 45 | security.pam.oath.enable = mkOption { 46 | visible = false; 47 | type = types.bool; 48 | default = false; 49 | }; 50 | security.pam.mount.enable = mkOption { 51 | visible = false; 52 | type = types.bool; 53 | default = false; 54 | }; 55 | services.samba.syncPasswordsByPam = mkOption { 56 | visible = false; 57 | type = types.bool; 58 | default = false; 59 | }; 60 | virtualisation.lxc.lxcfs.enable = mkOption { 61 | visible = false; 62 | type = types.bool; 63 | default = false; 64 | }; 65 | boot.specialFileSystems = mkOption { 66 | visible = false; 67 | default = []; 68 | }; 69 | boot.isContainer = mkOption { 70 | visible = false; 71 | default = true; 72 | }; 73 | programs.ssh.package = mkOption { 74 | visible = false; 75 | default = pkgs.openssh; 76 | }; 77 | 78 | systemd.tmpfiles = mkOption { 79 | visible = false; 80 | default = {}; 81 | }; 82 | 83 | users = { 84 | users = 85 | let fakeOptions = { 86 | openssh = {}; 87 | }; 88 | in mkOption { options = [ fakeOptions ];}; 89 | ldap.enable = mkOption { 90 | visible = false; 91 | type = types.bool; 92 | default = false; 93 | }; 94 | }; 95 | 96 | networking.proxy.envVars = mkOption { 97 | visible = false; 98 | type = types.attrs; 99 | default = {}; 100 | }; 101 | 102 | boot.supportedFilesystems = mkOption { 103 | default = [ ]; 104 | example = [ "btrfs" ]; 105 | type = types.listOf types.str; 106 | description = "Names of supported filesystem types in the initial ramdisk."; 107 | }; 108 | 109 | # Required by nginx 110 | security.acme = mkOption { 111 | visible = false; 112 | default = {}; 113 | }; 114 | # Required by nginx 115 | networking.enableIPv6 = mkOption { 116 | visible = false; 117 | default = false; 118 | }; 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /modules/image.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ...}: 2 | 3 | with lib; 4 | 5 | let 6 | cfg = config; 7 | in 8 | { 9 | options = { 10 | image = { 11 | name = mkOption { 12 | type = types.str; 13 | description = '' 14 | The name of the image 15 | ''; 16 | }; 17 | tag = mkOption { 18 | default = null; 19 | type = types.nullOr types.str; 20 | description = '' 21 | The tag of the image 22 | ''; 23 | }; 24 | run = mkOption { 25 | type = types.lines; 26 | default = ""; 27 | description = '' 28 | Extra commands run at container build time 29 | ''; 30 | }; 31 | env = mkOption { 32 | type = types.attrs; 33 | default = {}; 34 | description = '' 35 | Environment variables 36 | ''; 37 | }; 38 | entryPoint = mkOption { 39 | type = types.listOf types.str; 40 | default = []; 41 | description = '' 42 | Entry point command list 43 | ''; 44 | }; 45 | exposedPorts = mkOption { 46 | type = types.attrs; 47 | default = {}; 48 | description = '' 49 | Ports exposed by the container 50 | ''; 51 | }; 52 | from = mkOption { 53 | type = types.nullOr types.package; 54 | default = null; 55 | description = '' 56 | The parent image 57 | ''; 58 | }; 59 | interactive = mkOption { 60 | type = types.bool; 61 | default = false; 62 | description = '' 63 | Add packages for an interactive use of the container 64 | (bashInteractive, coreutils) 65 | ''; 66 | }; 67 | }; 68 | }; 69 | 70 | config = { 71 | image.run = '' 72 | mkdir -m 777 tmp 73 | ''; 74 | environment.systemPackages = optionals cfg.image.interactive [ pkgs.bashInteractive pkgs.coreutils ]; 75 | }; 76 | 77 | } 78 | -------------------------------------------------------------------------------- /modules/nix-daemon.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | 7 | cfg = config.nix; 8 | 9 | nix = cfg.package.out; 10 | 11 | isNix20 = versionAtLeast (getVersion nix) "2.0pre"; 12 | 13 | makeNixBuildUser = nr: 14 | { name = "nixbld${toString nr}"; 15 | description = "Nix build user ${toString nr}"; 16 | 17 | /* For consistency with the setgid(2), setuid(2), and setgroups(2) 18 | calls in `libstore/build.cc', don't add any supplementary group 19 | here except "nixbld". */ 20 | uid = builtins.add config.ids.uids.nixbld nr; 21 | group = "nixbld"; 22 | extraGroups = [ "nixbld" ]; 23 | }; 24 | 25 | nixbldUsers = map makeNixBuildUser (range 1 cfg.nrBuildUsers); 26 | 27 | nixConf = 28 | let 29 | # In Nix < 2.0, If we're using sandbox for builds, then provide 30 | # /bin/sh in the sandbox as a bind-mount to bash. This means we 31 | # also need to include the entire closure of bash. Nix >= 2.0 32 | # provides a /bin/sh by default. 33 | sh = pkgs.runtimeShell; 34 | binshDeps = pkgs.writeReferencesToFile sh; 35 | in 36 | pkgs.runCommand "nix.conf" { extraOptions = cfg.extraOptions; } ('' 37 | ${optionalString (!isNix20) '' 38 | extraPaths=$(for i in $(cat ${binshDeps}); do if test -d $i; then echo $i; fi; done) 39 | ''} 40 | cat > $out </dev/null 72 | '') 73 | ); 74 | 75 | in 76 | 77 | { 78 | 79 | ###### interface 80 | 81 | options = { 82 | 83 | nix = { 84 | 85 | enable = mkOption { 86 | description = "Whether enable Nix module."; 87 | type = types.bool; 88 | default = false; 89 | }; 90 | 91 | package = mkOption { 92 | type = types.package; 93 | default = pkgs.nix; 94 | defaultText = "pkgs.nix"; 95 | description = '' 96 | This option specifies the Nix package instance to use throughout the system. 97 | ''; 98 | }; 99 | 100 | maxJobs = mkOption { 101 | type = types.either types.int (types.enum ["auto"]); 102 | default = 1; 103 | example = 64; 104 | description = '' 105 | This option defines the maximum number of jobs that Nix will try 106 | to build in parallel. The default is 1. You should generally 107 | set it to the total number of logical cores in your system (e.g., 16 108 | for two CPUs with 4 cores each and hyper-threading). 109 | ''; 110 | }; 111 | 112 | autoOptimiseStore = mkOption { 113 | type = types.bool; 114 | default = false; 115 | example = true; 116 | description = '' 117 | If set to true, Nix automatically detects files in the store that have 118 | identical contents, and replaces them with hard links to a single copy. 119 | This saves disk space. If set to false (the default), you can still run 120 | nix-store --optimise to get rid of duplicate files. 121 | ''; 122 | }; 123 | 124 | buildCores = mkOption { 125 | type = types.int; 126 | default = 0; 127 | example = 64; 128 | description = '' 129 | This option defines the maximum number of concurrent tasks during 130 | one build. It affects, e.g., -j option for make. 131 | The special value 0 means that the builder should use all 132 | available CPU cores in the system. Some builds may become 133 | non-deterministic with this option; use with care! Packages will 134 | only be affected if enableParallelBuilding is set for them. 135 | ''; 136 | }; 137 | 138 | useSandbox = mkOption { 139 | type = types.either types.bool (types.enum ["relaxed"]); 140 | default = true; 141 | description = " 142 | If set, Nix will perform builds in a sandboxed environment that it 143 | will set up automatically for each build. This prevents impurities 144 | in builds by disallowing access to dependencies outside of the Nix 145 | store by using network and mount namespaces in a chroot environment. 146 | This is enabled by default even though it has a possible performance 147 | impact due to the initial setup time of a sandbox for each build. It 148 | doesn't affect derivation hashes, so changing this option will not 149 | trigger a rebuild of packages. 150 | "; 151 | }; 152 | 153 | sandboxPaths = mkOption { 154 | type = types.listOf types.str; 155 | default = []; 156 | example = [ "/dev" "/proc" ]; 157 | description = 158 | '' 159 | Directories from the host filesystem to be included 160 | in the sandbox. 161 | ''; 162 | }; 163 | 164 | extraOptions = mkOption { 165 | type = types.lines; 166 | default = ""; 167 | example = '' 168 | gc-keep-outputs = true 169 | gc-keep-derivations = true 170 | ''; 171 | description = "Additional text appended to nix.conf."; 172 | }; 173 | 174 | distributedBuilds = mkOption { 175 | type = types.bool; 176 | default = false; 177 | description = '' 178 | Whether to distribute builds to the machines listed in 179 | . 180 | ''; 181 | }; 182 | 183 | daemonNiceLevel = mkOption { 184 | type = types.int; 185 | default = 0; 186 | description = '' 187 | Nix daemon process priority. This priority propagates to build processes. 188 | 0 is the default Unix process priority, 19 is the lowest. 189 | ''; 190 | }; 191 | 192 | daemonIONiceLevel = mkOption { 193 | type = types.int; 194 | default = 0; 195 | description = '' 196 | Nix daemon process I/O priority. This priority propagates to build processes. 197 | 0 is the default Unix process I/O priority, 7 is the lowest. 198 | ''; 199 | }; 200 | 201 | buildMachines = mkOption { 202 | type = types.listOf types.attrs; 203 | default = []; 204 | example = literalExample '' 205 | [ { hostName = "voila.labs.cs.uu.nl"; 206 | sshUser = "nix"; 207 | sshKey = "/root/.ssh/id_buildfarm"; 208 | system = "powerpc-darwin"; 209 | maxJobs = 1; 210 | } 211 | { hostName = "linux64.example.org"; 212 | sshUser = "buildfarm"; 213 | sshKey = "/root/.ssh/id_buildfarm"; 214 | system = "x86_64-linux"; 215 | maxJobs = 2; 216 | speedFactor = 2; 217 | supportedFeatures = [ "kvm" ]; 218 | mandatoryFeatures = [ "perf" ]; 219 | } 220 | ] 221 | ''; 222 | description = '' 223 | This option lists the machines to be used if distributed 224 | builds are enabled (see 225 | ). Nix will perform 226 | derivations on those machines via SSH by copying the inputs 227 | to the Nix store on the remote machine, starting the build, 228 | then copying the output back to the local Nix store. Each 229 | element of the list should be an attribute set containing 230 | the machine's host name (hostname), the 231 | user name to be used for the SSH connection 232 | (sshUser), the Nix system type 233 | (system, e.g., 234 | "i686-linux"), the maximum number of 235 | jobs to be run in parallel on that machine 236 | (maxJobs), the path to the SSH private 237 | key to be used to connect (sshKey), a 238 | list of supported features of the machine 239 | (supportedFeatures) and a list of 240 | mandatory features of the machine 241 | (mandatoryFeatures). The SSH private key 242 | should not have a passphrase, and the corresponding public 243 | key should be added to 244 | ~sshUser/authorized_keys 245 | on the remote machine. 246 | ''; 247 | }; 248 | 249 | # Environment variables for running Nix. 250 | envVars = mkOption { 251 | type = types.attrs; 252 | internal = true; 253 | default = {}; 254 | description = "Environment variables used by Nix."; 255 | }; 256 | 257 | nrBuildUsers = mkOption { 258 | type = types.int; 259 | description = '' 260 | Number of nixbld user accounts created to 261 | perform secure concurrent builds. If you receive an error 262 | message saying that “all build users are currently in use”, 263 | you should increase this value. 264 | ''; 265 | }; 266 | 267 | readOnlyStore = mkOption { 268 | type = types.bool; 269 | default = true; 270 | description = '' 271 | If set, NixOS will enforce the immutability of the Nix store 272 | by making /nix/store a read-only bind 273 | mount. Nix will automatically make the store writable when 274 | needed. 275 | ''; 276 | }; 277 | 278 | binaryCaches = mkOption { 279 | type = types.listOf types.str; 280 | default = [ https://cache.nixos.org/ ]; 281 | description = '' 282 | List of binary cache URLs used to obtain pre-built binaries 283 | of Nix packages. 284 | ''; 285 | }; 286 | 287 | trustedBinaryCaches = mkOption { 288 | type = types.listOf types.str; 289 | default = [ ]; 290 | example = [ http://hydra.nixos.org/ ]; 291 | description = '' 292 | List of binary cache URLs that non-root users can use (in 293 | addition to those specified using 294 | ) by passing 295 | --option binary-caches to Nix commands. 296 | ''; 297 | }; 298 | 299 | requireSignedBinaryCaches = mkOption { 300 | type = types.bool; 301 | default = true; 302 | description = '' 303 | If enabled (the default), Nix will only download binaries from binary caches if 304 | they are cryptographically signed with any of the keys listed in 305 | . If disabled, signatures are neither 306 | required nor checked, so it's strongly recommended that you use only 307 | trustworthy caches and https to prevent man-in-the-middle attacks. 308 | ''; 309 | }; 310 | 311 | binaryCachePublicKeys = mkOption { 312 | type = types.listOf types.str; 313 | example = [ "hydra.nixos.org-1:CNHJZBh9K4tP3EKF6FkkgeVYsS3ohTl+oS0Qa8bezVs=" ]; 314 | description = '' 315 | List of public keys used to sign binary caches. If 316 | is enabled, 317 | then Nix will use a binary from a binary cache if and only 318 | if it is signed by any of the keys 319 | listed here. By default, only the key for 320 | cache.nixos.org is included. 321 | ''; 322 | }; 323 | 324 | trustedUsers = mkOption { 325 | type = types.listOf types.str; 326 | default = [ "root" ]; 327 | example = [ "root" "alice" "@wheel" ]; 328 | description = '' 329 | A list of names of users that have additional rights when 330 | connecting to the Nix daemon, such as the ability to specify 331 | additional binary caches, or to import unsigned NARs. You 332 | can also specify groups by prefixing them with 333 | @; for instance, 334 | @wheel means all users in the wheel 335 | group. 336 | ''; 337 | }; 338 | 339 | allowedUsers = mkOption { 340 | type = types.listOf types.str; 341 | default = [ "*" ]; 342 | example = [ "@wheel" "@builders" "alice" "bob" ]; 343 | description = '' 344 | A list of names of users (separated by whitespace) that are 345 | allowed to connect to the Nix daemon. As with 346 | , you can specify groups by 347 | prefixing them with @. Also, you can 348 | allow all users by specifying *. The 349 | default is *. Note that trusted users are 350 | always allowed to connect. 351 | ''; 352 | }; 353 | 354 | nixPath = mkOption { 355 | type = types.listOf types.str; 356 | default = 357 | [ 358 | "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos" 359 | "nixos-config=/etc/nixos/configuration.nix" 360 | "/nix/var/nix/profiles/per-user/root/channels" 361 | ]; 362 | description = '' 363 | The default Nix expression search path, used by the Nix 364 | evaluator to look up paths enclosed in angle brackets 365 | (e.g. <nixpkgs>). 366 | ''; 367 | }; 368 | 369 | checkConfig = mkOption { 370 | type = types.bool; 371 | default = true; 372 | description = '' 373 | If enabled (the default), checks that Nix can parse the generated nix.conf. 374 | ''; 375 | }; 376 | }; 377 | 378 | }; 379 | 380 | 381 | ###### implementation 382 | 383 | config = mkIf cfg.enable { 384 | 385 | nix.binaryCachePublicKeys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; 386 | 387 | environment.etc."nix/nix.conf".source = nixConf; 388 | 389 | # List of machines for distributed Nix builds in the format 390 | # expected by build-remote.pl. 391 | environment.etc."nix/machines" = 392 | { enable = cfg.buildMachines != []; 393 | text = 394 | concatMapStrings (machine: 395 | "${if machine ? sshUser then "${machine.sshUser}@" else ""}${machine.hostName} " 396 | + machine.system or (concatStringsSep "," machine.systems) 397 | + " ${machine.sshKey or "-"} ${toString machine.maxJobs or 1} " 398 | + toString (machine.speedFactor or 1) 399 | + " " 400 | + concatStringsSep "," (machine.mandatoryFeatures or [] ++ machine.supportedFeatures or []) 401 | + " " 402 | + concatStringsSep "," machine.mandatoryFeatures or [] 403 | + "\n" 404 | ) cfg.buildMachines; 405 | }; 406 | 407 | systemd.packages = [ nix ]; 408 | 409 | systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ]; 410 | 411 | systemd.services.nix-daemon = 412 | { path = [ nix pkgs.utillinux config.programs.ssh.package ] 413 | ++ optionals cfg.distributedBuilds [ pkgs.gzip ] 414 | ++ optionals (!isNix20) [ pkgs.openssl.bin ]; 415 | 416 | environment = cfg.envVars 417 | // { CURL_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; } 418 | // config.networking.proxy.envVars; 419 | 420 | unitConfig.RequiresMountsFor = "/nix/store"; 421 | 422 | serviceConfig = 423 | { Nice = cfg.daemonNiceLevel; 424 | IOSchedulingPriority = cfg.daemonIONiceLevel; 425 | LimitNOFILE = 4096; 426 | }; 427 | 428 | restartTriggers = [ nixConf ]; 429 | }; 430 | 431 | nix.envVars = 432 | optionalAttrs (!isNix20) { 433 | NIX_CONF_DIR = "/etc/nix"; 434 | 435 | # Enable the copy-from-other-stores substituter, which allows 436 | # builds to be sped up by copying build results from remote 437 | # Nix stores. To do this, mount the remote file system on a 438 | # subdirectory of /run/nix/remote-stores. 439 | NIX_OTHER_STORES = "/run/nix/remote-stores/*/nix"; 440 | } 441 | 442 | // optionalAttrs (cfg.distributedBuilds && !isNix20) { 443 | NIX_BUILD_HOOK = "${nix}/libexec/nix/build-remote.pl"; 444 | }; 445 | 446 | # Set up the environment variables for running Nix. 447 | environment.sessionVariables = cfg.envVars // 448 | { NIX_PATH = cfg.nixPath; 449 | }; 450 | 451 | environment.extraInit = optionalString (!isNix20) 452 | '' 453 | # Set up secure multi-user builds: non-root users build through the 454 | # Nix daemon. 455 | if [ "$USER" != root -o ! -w /nix/var/nix/db ]; then 456 | export NIX_REMOTE=daemon 457 | fi 458 | '' + '' 459 | if [ -e "$HOME/.nix-defexpr/channels" ]; then 460 | export NIX_PATH="$HOME/.nix-defexpr/channels''${NIX_PATH:+:$NIX_PATH}" 461 | fi 462 | ''; 463 | 464 | nix.nrBuildUsers = mkDefault (lib.max 32 cfg.maxJobs); 465 | 466 | users.users = nixbldUsers; 467 | 468 | services.xserver.displayManager.hiddenUsers = map ({ name, ... }: name) nixbldUsers; 469 | 470 | # FIXME: use systemd-tmpfiles to create Nix directories. 471 | system.activationScripts.nix = stringAfter [ "etc" "users" ] 472 | '' 473 | # Nix initialisation. 474 | install -m 0755 -d \ 475 | /nix/var/nix/gcroots \ 476 | /nix/var/nix/temproots \ 477 | /nix/var/nix/userpool \ 478 | /nix/var/nix/profiles \ 479 | /nix/var/nix/db \ 480 | /nix/var/log/nix/drvs 481 | install -m 1777 -d \ 482 | /nix/var/nix/gcroots/per-user \ 483 | /nix/var/nix/profiles/per-user \ 484 | /nix/var/nix/gcroots/tmp 485 | ''; 486 | 487 | }; 488 | 489 | } 490 | -------------------------------------------------------------------------------- /modules/s6-lib.nix: -------------------------------------------------------------------------------- 1 | # FIXME: This file should be moved to lib/ but I fail to make it 2 | # available for the NixOS module system... 3 | 4 | { pkgs }: 5 | 6 | with pkgs.lib; 7 | 8 | let 9 | envDir = env: pkgs.runCommand "env-dir" {} ('' 10 | mkdir $out 11 | '' + concatStringsSep "\n" (mapAttrsToList (n: v: "echo ${v} > $out/${n}") env)); 12 | 13 | # The init script binary environment 14 | # 15 | # Init script uses absolute path for binaries. This is a little bit 16 | # verbose, but it simplifies a lot the PATH environment variable 17 | # managment. Since init script itself doesn't rely on the PATH, we 18 | # can easily propagate the PATH variable from from the environment 19 | # to service scripts. 20 | e = let 21 | env = pkgs.buildEnv { 22 | name = "init-path"; 23 | paths = with pkgs; [ execline s6PortableUtils s6 coreutils ]; 24 | }; 25 | in "${env}/bin"; 26 | 27 | in rec { 28 | 29 | s6InitWithStateDir = args: pkgs.writeTextFile { 30 | name = "init-with-state-dir"; 31 | executable = true; 32 | text = '' 33 | #!${pkgs.execline}/bin/execlineb -S0 34 | ${s6Init args} "/run/s6" 35 | ''; 36 | }; 37 | 38 | s6Init = { 39 | oneshotPres, 40 | oneshotPosts, 41 | longRuns, 42 | # If true, all processes are killed in s6 finish script! 43 | inPidNamespace ? false 44 | }: pkgs.writeTextFile { 45 | name = "init"; 46 | executable = true; 47 | text = '' 48 | #!${e}/execlineb -S0 49 | 50 | ${e}/if { 51 | 52 | ${e}/ifelse { ${e}/s6-test $# -ne 1 } 53 | { ${e}/if { ${e}/s6-echo "Usage: $0 STATE-DIR" } exit 1 } 54 | 55 | ${e}/ifelse { ${e}/s6-test -e $1 } 56 | { ${e}/if { ${e}/s6-echo The state directory $1 must not exist! Exiting. } ${e}/exit 1 } 57 | 58 | ${e}/if { ${e}/s6-echo [init stage 1] Starting } 59 | 60 | ${e}/if { ${e}/s6-mkdir -p ''${1}/.s6-svscan } 61 | ${e}/if { ${e}/s6-ln -s ${genFinish inPidNamespace} ''${1}/.s6-svscan/finish } 62 | 63 | # Init stage 2 64 | ${e}/background { 65 | ${e}/if { ${e}/s6-echo [init stage 2] Running oneshot services } 66 | 67 | ${genOneshots oneshotPres} 68 | 69 | ${e}/if { ${e}/s6-echo [init stage 2] Activate longrun services } 70 | 71 | # Move the service file to the scan directoy 72 | ${e}/if { ${e}/cp -r ${genS6ScanDir longRuns}/. $1 } 73 | # To be able to delete the state dir 74 | ${e}/if { ${e}/chmod -R 0755 $1 } 75 | 76 | ${e}/if { ${e}/s6-svscanctl -a $1 } 77 | 78 | ${genOneshots oneshotPosts} 79 | } 80 | 81 | ## run the rest of stage 1 with sanitized descriptors 82 | ${e}/redirfd -r 0 /dev/null 83 | ${e}/true 84 | } 85 | 86 | # Run the pid 1 87 | ${e}/s6-svscan -t0 $1 88 | ''; 89 | }; 90 | 91 | # If a oneshost service fails, the s6-svscan process (which could be 92 | # the pid 1) if stopped. 93 | genOneshots = concatMapStringsSep "\n " (s: '' 94 | ${e}/foreground { 95 | ${e}/if -X -n 96 | { ${e}/foreground 97 | { ${e}/s6-echo [init stage 2] Start oneshot service "${s.name}" } 98 | ${genS6Run s} 99 | } 100 | # If the oneshot service fails, s6-svscan is stopped 101 | ${e}/foreground 102 | { ${e}/s6-echo [init stage 2] Oneshot service '${s.name}' failed } 103 | ${e}/if -n 104 | { ${e}/s6-test -v S6_DONT_TERMINATE_ON_ERROR } 105 | ${e}/foreground { ${e}/s6-svscanctl -t $1 } 106 | ${e}/exit 1 107 | } 108 | ''); 109 | 110 | genFinish = inPidNamespace: if inPidNamespace 111 | then pkgs.writeScript "s6-finish" '' 112 | #!${e}/execlineb -S0 113 | 114 | # Sync before TERM'n 115 | ${e}/foreground { ${e}/s6-echo "[init stage 3] syncing disks." } 116 | ${e}/foreground { ${e}/s6-sync } 117 | 118 | # Kill everything, gently. 119 | ${e}/foreground { ${e}/s6-echo "[init stage 3] sending all processes the TERM signal." } 120 | ${e}/foreground { ${e}/s6-nuke -th } # foreground is process 1: it survives 121 | ${e}/foreground { ${e}/s6-sleep 3 } 122 | 123 | # Last message, then close our pipes and give the logger some time. 124 | ${e}/foreground { ${e}/s6-echo "[init stage 3] sending all processes the KILL signal and exiting." } 125 | ${e}/fdclose 1 ${e}/fdclose 2 126 | ${e}/s6-sleep -m 200 127 | 128 | # Kill everything, brutally. 129 | ${e}/foreground { ${e}/s6-nuke -k } # foreground is process 1: it survives again 130 | 131 | # Reap all the zombies then sync, and we're done. 132 | ${e}/wait { } 133 | ${e}/foreground { ${e}/s6-sync } 134 | '' 135 | else pkgs.writeScript "s6-finish" '' 136 | #!${e}/s6-echo [init stage 3] Soft finish because not in a pid namespace! 137 | ''; 138 | 139 | genS6ScanDir = services: pkgs.runCommand "s6-scandir" {} ('' 140 | mkdir -p $out 141 | '' + (concatMapStringsSep "\n" (genS6ServiceDir) services)); 142 | 143 | genS6ServiceDir = service: '' 144 | mkdir $out/${service.name} 145 | ln -s ${genS6Run service} $out/${service.name}/run 146 | ln -s ${genS6Finish service} $out/${service.name}/finish 147 | ${optionalString (service.execLogger != null) '' 148 | mkdir $out/${service.name}/log 149 | ln -s ${genS6Log service} $out/${service.name}/log/run 150 | '' 151 | } 152 | ''; 153 | 154 | genS6Run = { 155 | name, 156 | type, 157 | environment, 158 | execStart, 159 | workingDirectory, 160 | user, 161 | ... 162 | }: 163 | pkgs.writeTextFile { 164 | name = "${name}-run"; 165 | executable = true; 166 | text = '' 167 | #!${e}/execlineb -P 168 | ${e}/fdmove -c 2 1 169 | ${optionalString (user != "root") "${e}/s6-setuidgid ${user}"} 170 | ${optionalString (workingDirectory != null) "${e}/cd ${workingDirectory}"} 171 | ${optionalString (environment != {}) "${e}/s6-envdir ${envDir environment}"} 172 | ${execStart} 173 | ''; 174 | }; 175 | 176 | genS6Finish = { name, restartOnFailure, ... }: pkgs.writeTextFile { 177 | name = "${name}-finish"; 178 | executable = true; 179 | text = '' 180 | #!${e}/execlineb -S0 181 | ${e}/foreground { ${e}/s6-echo "[init] Service '${name}' terminates with exit code $1" } 182 | '' + 183 | (if (restartOnFailure == false) 184 | then '' 185 | ${e}/if { ${e}/s6-test $\{1} -ne 0 } 186 | ${e}/if { ${e}/s6-test $\{1} -ne 256 } 187 | ${e}/foreground { ${e}/s6-svc -d ./ } 188 | ${e}/if -n 189 | { ${e}/s6-test -v S6_DONT_TERMINATE_ON_ERROR } 190 | ${e}/s6-svscanctl -t ../ 191 | '' 192 | else '' 193 | ${e}/foreground { ${e}/s6-echo "[init] Service '${name}' will be restarted" } 194 | ''); 195 | }; 196 | 197 | genS6Log = { name, execLogger, user, ... }: 198 | pkgs.writeTextFile { 199 | name = "${name}-log"; 200 | executable = true; 201 | text = '' 202 | #!${e}/execlineb -P 203 | ${execLogger} 204 | ''; 205 | }; 206 | } 207 | -------------------------------------------------------------------------------- /modules/s6.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ...}: 2 | 3 | with import ./s6-lib.nix { inherit pkgs; }; 4 | with lib; 5 | 6 | let 7 | cfg = config; 8 | 9 | # Sort oneshot services based on their dependencies. 10 | # FIXME: abort on cycles by checking .cycle attribute 11 | oneshotsSorted = oneshots: let 12 | t = toposort (a: b: elem a.name b.after ) oneshots; 13 | in t.result; 14 | 15 | toAttrs = mapAttrsToList (name: v: v // { inherit name; }); 16 | filtered = filterAttrs (n: v: v.enable) cfg.s6.services; 17 | 18 | s6ServicesMakeInitArgs = let 19 | services = toAttrs filtered; 20 | oneshotPres = filter (v: v.type == "oneshot-pre") services; 21 | longRuns = filter (v: v.type == "long-run") services; 22 | oneshotPosts = filter (v: v.type == "oneshot-post") services; 23 | in { 24 | oneshotPres = (oneshotsSorted oneshotPres); 25 | oneshotPosts = (oneshotsSorted oneshotPosts); 26 | longRuns = longRuns; 27 | }; 28 | 29 | entryPoint = s6InitWithStateDir (s6ServicesMakeInitArgs // { inPidNamespace = true; }); 30 | 31 | s6ServiceConfig = { name, config, ...}: 32 | { 33 | config = mkMerge [ 34 | (mkIf (config.script != "") 35 | { execStart = pkgs.writeScript "s6-script-${name}" '' 36 | #! ${pkgs.runtimeShell} -e 37 | ${config.script} 38 | ''; }) 39 | ]; 40 | }; 41 | 42 | s6ServiceOptions = { 43 | enable = mkOption { 44 | default = true; 45 | type = types.bool; 46 | description = "Whether to enable the service"; 47 | }; 48 | execStart = mkOption { 49 | type = with types; either str package; 50 | default = ""; 51 | description = "Command executed as the service's main process."; 52 | }; 53 | script = mkOption { 54 | type = types.lines; 55 | default = ""; 56 | description = "Shell commands executed as the service's main process."; 57 | }; 58 | type = mkOption { 59 | default = "long-run"; 60 | type = types.enum ["long-run" "oneshot-pre" "oneshot-post"]; 61 | description = "Type of the s6 service (oneshot-pre, long-run or oneshot-post)."; 62 | }; 63 | restartOnFailure = mkOption { 64 | default = false; 65 | type = types.bool; 66 | description = "Restart the service if it fails. Note this is only used by long-run services."; 67 | }; 68 | workingDirectory = mkOption { 69 | default = null; 70 | type = types.nullOr types.str; 71 | description = "Sets the working directory for executed processes."; 72 | }; 73 | user = mkOption { 74 | default = "root"; 75 | type = types.str; 76 | description = "Set the UNIX user that the processes are executed as."; 77 | }; 78 | environment = mkOption { 79 | default = {}; 80 | type = with types; attrsOf (nullOr (either str (either path package))); 81 | example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; }; 82 | description = "Environment variables passed to the service's processes."; 83 | }; 84 | after = mkOption { 85 | default = []; 86 | type = types.listOf types.str; 87 | description = "Configure ordering dependencies between units."; 88 | }; 89 | execLogger = mkOption { 90 | default = null; 91 | type = with types; nullOr (either str package); 92 | description = "Command executed as the service's logger: it gets the stdout of the main process."; 93 | }; 94 | }; 95 | 96 | in 97 | 98 | { 99 | options = { 100 | s6.init = mkOption { 101 | type = types.nullOr types.package; 102 | default = null; 103 | description = "The generated init script."; 104 | }; 105 | 106 | s6.services = mkOption { 107 | default = {}; 108 | type = with types; attrsOf (submodule [{ options = s6ServiceOptions; } s6ServiceConfig ]); 109 | description = "Definition of s6 service."; 110 | }; 111 | }; 112 | 113 | # The s6 image entry point is only set if some services are defined 114 | config = mkIf (cfg.s6.services != {}) { 115 | s6.init = s6Init s6ServicesMakeInitArgs; 116 | image.entryPoint = [ "${entryPoint}" ]; 117 | environment.systemPackages = [ pkgs.s6 ]; 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /modules/system-path.nix: -------------------------------------------------------------------------------- 1 | # This is copied from nixos/modules/config/system-path.nix and patched 2 | # to remove all required packages. 3 | 4 | # This module defines the packages that appear in 5 | # /run/current-system/sw. 6 | 7 | { config, lib, pkgs, ... }: 8 | 9 | with lib; 10 | 11 | { 12 | options = { 13 | 14 | environment = { 15 | 16 | systemPackages = mkOption { 17 | type = types.listOf types.package; 18 | default = []; 19 | example = literalExample "[ pkgs.firefox pkgs.thunderbird ]"; 20 | description = '' 21 | The set of packages that appear in 22 | /run/current-system/sw. These packages are 23 | automatically available to all users, and are 24 | automatically updated every time you rebuild the system 25 | configuration. (The latter is the main difference with 26 | installing them in the default profile, 27 | /nix/var/nix/profiles/default. 28 | ''; 29 | }; 30 | 31 | pathsToLink = mkOption { 32 | type = types.listOf types.str; 33 | # Note: We need `/lib' to be among `pathsToLink' for NSS modules 34 | # to work. 35 | default = []; 36 | example = ["/"]; 37 | description = "List of directories to be symlinked in /run/current-system/sw."; 38 | }; 39 | 40 | extraOutputsToInstall = mkOption { 41 | type = types.listOf types.str; 42 | default = [ ]; 43 | example = [ "doc" "info" "devdoc" ]; 44 | description = "List of additional package outputs to be symlinked into /run/current-system/sw."; 45 | }; 46 | 47 | extraSetup = mkOption { 48 | type = types.lines; 49 | default = ""; 50 | description = "Shell fragments to be run after the system environment has been created. This should only be used for things that need to modify the internals of the environment, e.g. generating MIME caches. The environment being built can be accessed at $out."; 51 | }; 52 | 53 | }; 54 | 55 | system = { 56 | 57 | path = mkOption { 58 | internal = true; 59 | description = '' 60 | The packages you want in the boot environment. 61 | ''; 62 | }; 63 | 64 | }; 65 | 66 | }; 67 | 68 | config = { 69 | 70 | environment.pathsToLink = 71 | [ "/bin" 72 | "/etc/xdg" 73 | "/etc/gtk-2.0" 74 | "/etc/gtk-3.0" 75 | "/lib" # FIXME: remove and update debug-info.nix 76 | "/sbin" 77 | "/share/emacs" 78 | "/share/nano" 79 | "/share/org" 80 | "/share/themes" 81 | "/share/vim-plugins" 82 | "/share/vulkan" 83 | "/share/kservices5" 84 | "/share/kservicetypes5" 85 | "/share/kxmlgui5" 86 | ]; 87 | 88 | system.path = pkgs.buildEnv { 89 | name = "system-path"; 90 | paths = config.environment.systemPackages; 91 | inherit (config.environment) pathsToLink extraOutputsToInstall; 92 | ignoreCollisions = true; 93 | # !!! Hacky, should modularise. 94 | # outputs TODO: note that the tools will often not be linked by default 95 | postBuild = 96 | '' 97 | if [ -x $out/bin/glib-compile-schemas -a -w $out/share/glib-2.0/schemas ]; then 98 | $out/bin/glib-compile-schemas $out/share/glib-2.0/schemas 99 | fi 100 | 101 | ${config.environment.extraSetup} 102 | ''; 103 | }; 104 | 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /modules/system.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ...}: 2 | 3 | with lib; 4 | 5 | { 6 | options = { 7 | system.build = mkOption { 8 | internal = true; 9 | default = {}; 10 | type = types.attrs; 11 | description = '' 12 | Attribute set of derivations used to setup the system. 13 | ''; 14 | }; 15 | }; 16 | 17 | config = { 18 | # This is to remove sytemd dependencies 19 | # { startSession = true; allowNullPassword = true; showMotd = true; updateWtmp = true; } 20 | security.pam.services.login = mkOverride 1 { startSession = false; }; 21 | 22 | environment.extraInit = '' 23 | export PATH=/bin:$PATH 24 | ''; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /modules/systemd.nix: -------------------------------------------------------------------------------- 1 | # Systemd services are converted to s6 services. Be careful, the 2 | # semantic of systemd service is not respected! See the README for 3 | # details. 4 | 5 | { pkgs, config, lib, ...}: 6 | 7 | # The systemd interface! 8 | with import (pkgs.path + "/nixos/modules/system/boot/systemd-unit-options.nix") { inherit config lib; }; 9 | 10 | with lib; 11 | 12 | let 13 | cfg = config; 14 | 15 | serviceConfig = { name, config, ... }: { 16 | config = mkMerge 17 | [ { # Default path for systemd services. Should be quite minimal. 18 | # TODO: adapt that to s6 19 | path = 20 | [ pkgs.coreutils 21 | pkgs.findutils 22 | pkgs.gnugrep 23 | pkgs.gnused 24 | ]; 25 | environment.PATH = config.path; 26 | } 27 | (mkIf (config.preStart != "") 28 | { serviceConfig.ExecStartPre = config.preStart; }) 29 | ]; 30 | }; 31 | 32 | systemd.services = mkOption { 33 | default = {}; 34 | type = with types; attrsOf (submodule [ { options = serviceOptions; } serviceConfig ]); 35 | description = "Definition of systemd service units."; 36 | }; 37 | systemd.packages = {}; 38 | systemd.sockets = {}; 39 | 40 | # Generate s6 arguments from systemd service definition 41 | systemdToS6 = name: service: 42 | let 43 | # execStart can start with special characters that needs to be 44 | # interpreted. 45 | # @/bin/service service arg1 -> exec -a service /bin/service arg1 46 | execStartToS6 = e: 47 | let l = splitString " " e; 48 | in if pkgs.lib.hasPrefix "@" (head l) 49 | then concatStringsSep " " ([ 50 | "exec" 51 | "-a" 52 | (elemAt l 1) 53 | (pkgs.lib.removePrefix "@" (head l)) 54 | ] ++ (drop 2 l)) 55 | else e; 56 | type = let 57 | t = attrByPath ["serviceConfig" "Type"] "simple" service; 58 | in (if (t == "simple") 59 | then "long-run" 60 | else if (t == "oneshot" && isOneshotPost service) 61 | then "oneshot-post" 62 | else "oneshot-pre"); 63 | script = let 64 | start = if hasAttrByPath ["serviceConfig" "ExecStart"] service 65 | then execStartToS6 service.serviceConfig.ExecStart 66 | else service.script; 67 | in attrByPath ["serviceConfig" "ExecStartPre"] "" service + "\n" + start; 68 | in 69 | { 70 | inherit type script; 71 | environment = service.environment; 72 | workingDirectory = attrByPath ["serviceConfig" "WorkingDirectory"] null service; 73 | # By default, it is false 74 | restartOnFailure = attrByPath ["serviceConfig" "Restart"] "no" service == "always"; 75 | after = map (removeSuffix ".service") (attrByPath ["after"] [] service); 76 | }; 77 | 78 | # TODO: print a warning on unsupported services 79 | supportedSystemdServices = let 80 | predicates = n: v: all (f: f n v) [ 81 | # Units containing calendar event are not supported (cron jobs) 82 | (n: v: v.startAt == []) 83 | # Units have either ExecStart or a script 84 | (n: v: hasAttrByPath ["serviceConfig" "ExecStart"] v || (v.script != "")) 85 | # Only Restart values no and always are supported 86 | (n: v: (! hasAttrByPath ["serviceConfig" "Restart"] v) || (v.serviceConfig.Restart == "no") || (v.serviceConfig.Restart == "always")) 87 | ]; 88 | in 89 | filterAttrs predicates cfg.systemd.services; 90 | 91 | # If a oneshot service has a long run service in its after option, 92 | # this oneshot service is run after long run services. 93 | isOneshotPost = service: let 94 | type = attrByPath ["serviceConfig" "Type"] "simple"; 95 | isLongRun = name: any (s: name == (s.name + ".service") && (type s) == "simple") (mapAttrsToList (name: v: v // { inherit name; }) supportedSystemdServices); 96 | in any isLongRun service.after; 97 | 98 | in 99 | 100 | { 101 | options = { 102 | inherit systemd; 103 | }; 104 | config.s6.services = mapAttrs systemdToS6 supportedSystemdServices; 105 | } 106 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "nixpkgs": { 3 | "url": "https://github.com/NixOS/nixpkgs/archive/f52505fac8c82716872a616c501ad9eff188f97f.tar.gz", 4 | "owner": "NixOS", 5 | "branch": "19.03", 6 | "url_template": "https://github.com///archive/.tar.gz", 7 | "repo": "nixpkgs", 8 | "type": "tarball", 9 | "sha256": "0q2m2qhyga9yq29yz90ywgjbn9hdahs7i8wwlq7b55rdbyiwa5dy", 10 | "description": "Nix Packages collection", 11 | "rev": "f52505fac8c82716872a616c501ad9eff188f97f" 12 | } 13 | } -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | # A record, from name to path, of the third-party packages 4 | with rec 5 | { 6 | pkgs = 7 | if hasNixpkgsPath 8 | then 9 | if hasThisAsNixpkgsPath 10 | then import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {} 11 | else import {} 12 | else 13 | import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {}; 14 | 15 | sources_nixpkgs = 16 | if builtins.hasAttr "nixpkgs" sources 17 | then sources.nixpkgs 18 | else abort 19 | '' 20 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 21 | add a package called "nixpkgs" to your sources.json. 22 | ''; 23 | 24 | builtins_fetchTarball = 25 | # fetchTarball version that is compatible between all the versions of 26 | # Nix 27 | { url, sha256 }@attrs: 28 | let 29 | inherit (builtins) lessThan nixVersion fetchTarball; 30 | in 31 | if lessThan nixVersion "1.12" then 32 | fetchTarball { inherit url; } 33 | else 34 | fetchTarball attrs; 35 | 36 | hasNixpkgsPath = (builtins.tryEval ).success; 37 | hasThisAsNixpkgsPath = 38 | (builtins.tryEval ).success && == ./.; 39 | 40 | sources = builtins.fromJSON (builtins.readFile ./sources.json); 41 | 42 | mapAttrs = builtins.mapAttrs or 43 | (f: set: with builtins; 44 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))); 45 | 46 | getFetcher = spec: 47 | let fetcherName = 48 | if builtins.hasAttr "type" spec 49 | then builtins.getAttr "type" spec 50 | else "tarball"; 51 | in builtins.getAttr fetcherName { 52 | "tarball" = pkgs.fetchzip; 53 | "file" = pkgs.fetchurl; 54 | }; 55 | }; 56 | # NOTE: spec must _not_ have an "outPath" attribute 57 | mapAttrs (_: spec: 58 | if builtins.hasAttr "outPath" spec 59 | then abort 60 | "The values in sources.json should not have an 'outPath' attribute" 61 | else 62 | if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec 63 | then 64 | spec // 65 | { outPath = getFetcher spec { inherit (spec) url sha256; } ; } 66 | else spec 67 | ) sources 68 | -------------------------------------------------------------------------------- /overlay.nix: -------------------------------------------------------------------------------- 1 | self: super: 2 | 3 | let 4 | lib = import ./lib { pkgs = super; }; 5 | 6 | dockerImages = with lib; { 7 | nix = makeImage ./images/nix.nix; 8 | example = makeImage ./images/example.nix; 9 | example-systemd = makeImage ./images/example-systemd.nix; 10 | }; 11 | 12 | tests.nixContainerImages = { 13 | s6 = super.callPackages ./tests/s6.nix { }; 14 | readme = super.callPackages ./tests/readme.nix { }; 15 | minimalImageSize = super.callPackage ./tests/minimal-image-size.nix { }; 16 | dockerImages = { 17 | nix = super.callPackage ./tests/nix.nix { }; 18 | from = super.callPackage ./tests/from.nix { }; 19 | exposedPorts = super.callPackage ./tests/exposed-ports.nix { }; 20 | nginx = super.callPackage ./tests/nginx.nix { }; 21 | env = super.callPackage ./tests/env.nix { }; 22 | systemd = super.callPackage ./tests/systemd.nix { }; 23 | s6 = super.callPackage ./tests/s6-image.nix { }; 24 | }; 25 | }; 26 | in 27 | { 28 | inherit dockerImages; 29 | tests.nixContainerImages = tests.nixContainerImages; 30 | lib = super.lib // lib; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /tests/env.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, curl, gnugrep, coreutils }: 2 | 3 | with lib; 4 | 5 | let 6 | image = lib.makeImage { 7 | config = { 8 | image = { 9 | name = "env"; 10 | env = { 11 | KEY = "value"; 12 | }; 13 | }; 14 | environment.systemPackages = [ gnugrep coreutils ]; 15 | }; 16 | }; 17 | 18 | in 19 | 20 | lib.makeContainerTest { 21 | inherit image; 22 | testScript = '' 23 | $machine->waitForUnit("docker.service"); 24 | $machine->succeed("docker load -i ${image}"); 25 | $machine->succeed("docker run ${lib.imageRef image} env | grep -q KEY=value"); 26 | ''; 27 | } 28 | -------------------------------------------------------------------------------- /tests/exposed-ports.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, dockerTools, coreutils }: 2 | 3 | let 4 | image = lib.makeImage { 5 | config = { 6 | image = { 7 | name = "exposed-ports"; 8 | exposedPorts = { "5000/tcp" = {}; }; 9 | }; 10 | environment.systemPackages = [ coreutils ]; 11 | }; 12 | }; 13 | in lib.makeContainerTest { 14 | inherit image; 15 | testScript = '' 16 | $machine->waitForUnit("docker.service"); 17 | $machine->succeed("docker load -i ${image}"); 18 | $machine->succeed("docker run -d -P --name exposed-ports ${lib.imageRef image} sleep 10m"); 19 | $machine->succeed("docker port exposed-ports | grep '5000/tcp -> 0.0.0.0'"); 20 | ''; 21 | } 22 | -------------------------------------------------------------------------------- /tests/from.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, dockerTools }: 2 | 3 | let 4 | image = lib.makeImage { 5 | config.image = { 6 | name = "from"; 7 | from = dockerTools.pullImage { 8 | imageName = "alpine"; 9 | imageDigest = "sha256:46e71df1e5191ab8b8034c5189e325258ec44ea739bba1e5645cff83c9048ff1"; 10 | sha256 = "1xryr64b1s04l232ar5fk6gnlbikh8y1g3vg9g0kzwqyk36vxp81"; 11 | }; 12 | }; 13 | }; 14 | in lib.makeContainerTest { 15 | inherit image; 16 | testScript = '' 17 | $machine->waitForUnit("docker.service"); 18 | $machine->succeed("docker load -i ${image}"); 19 | ''; 20 | } 21 | -------------------------------------------------------------------------------- /tests/minimal-image-size.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | 3 | let 4 | minimal = lib.makeImage ( 5 | {config, pkgs, ...}: { 6 | config.image.name = "minimal"; 7 | } 8 | ); 9 | 10 | in 11 | pkgs.runCommand "minimal-image-size" {} '' 12 | set -e 13 | MINIMAL_SIZE=25000 14 | 15 | SIZE=$(du ${minimal} | cut -f1) 16 | if [[ $SIZE -ge $MINIMAL_SIZE ]]; 17 | then 18 | echo "Minimal image size should be less than $MINIMAL_SIZE while it is $SIZE" 19 | exit 1 20 | else 21 | echo "Minimal image size is $SIZE" | tee $out 22 | fi 23 | '' 24 | 25 | -------------------------------------------------------------------------------- /tests/nginx.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, curl }: 2 | 3 | with lib; 4 | 5 | let 6 | wwwDir = pkgs.runCommand "wwwDir" {} '' 7 | mkdir $out 8 | echo plop > $out/plop 9 | ''; 10 | 11 | image = lib.makeImage { 12 | config = { 13 | image = { 14 | name = "nginx"; 15 | }; 16 | services.nginx = { 17 | enable = true; 18 | virtualHosts.localhost.root = wwwDir; 19 | }; 20 | }; 21 | }; 22 | 23 | in 24 | 25 | lib.makeContainerTest { 26 | inherit image; 27 | testScript = '' 28 | $machine->waitForUnit("docker.service"); 29 | $machine->succeed("docker load -i ${image}"); 30 | $machine->succeed("docker run -d -p 8000:80 ${lib.imageRef image}"); 31 | $machine->waitUntilSucceeds("${curl}/bin/curl -f localhost:8000/plop"); 32 | ''; 33 | } 34 | -------------------------------------------------------------------------------- /tests/nix.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, dockerImages }: 2 | 3 | with lib; 4 | 5 | lib.makeContainerTest { 6 | image = dockerImages.nix; 7 | testScript = '' 8 | $machine->waitForUnit("docker.service"); 9 | $machine->succeed("docker load -i ${dockerImages.nix}"); 10 | $machine->succeed("docker run ${lib.imageRef dockerImages.nix} nix --version"); 11 | ''; 12 | } 13 | -------------------------------------------------------------------------------- /tests/readme.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | 3 | { 4 | readme1 = 5 | lib.makeImage ({ pkgs, ... }: { 6 | config.image = { 7 | name = "hello"; 8 | entryPoint = [ "${pkgs.hello}/bin/hello" ]; 9 | }; 10 | }); 11 | 12 | readme2 = 13 | lib.makeImage ({ pkgs, ... }: { 14 | config = { 15 | image.name = "s6"; 16 | s6.services.nginx = { 17 | execStart = ''${pkgs.nginx}/bin/nginx -g "daemon off;"''; 18 | }; 19 | }; 20 | }); 21 | 22 | readme3 = 23 | lib.makeImage ({ pkgs, ... }: { 24 | config = { 25 | image.name = "nixos"; 26 | environment.systemPackages = [ pkgs.coreutils ]; 27 | users.users.alice = { 28 | isNormalUser = true; 29 | }; 30 | }; 31 | }); 32 | readme4 = 33 | lib.makeImage ({ pkgs, ... }: { 34 | config = { 35 | image.name = "nginx"; 36 | # Yeah! It is the NixOS module! 37 | services.nginx.enable = true; 38 | }; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /tests/s6-image.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, jq }: 2 | 3 | with lib; 4 | 5 | let 6 | image = lib.makeImage { 7 | config = { 8 | image = { 9 | name = "s6"; 10 | }; 11 | s6.services.stopContainer = { 12 | script = '' 13 | echo stopContainer 14 | exit 1 15 | ''; 16 | }; 17 | }; 18 | }; 19 | 20 | in 21 | 22 | lib.makeContainerTest { 23 | inherit image; 24 | testScript = '' 25 | $machine->waitForUnit("docker.service"); 26 | $machine->succeed("docker load -i ${image}"); 27 | $machine->succeed("docker run --name container -d ${lib.imageRef image}"); 28 | $machine->waitUntilSucceeds("docker inspect container | ${jq}/bin/jq -e '.[].State.Status == \"exited\"'"); 29 | $machine->succeed("docker logs container | grep -q stopContainer"); 30 | $machine->succeed("docker logs container | grep -q 'sending all processes the TERM signal'"); 31 | ''; 32 | } 33 | -------------------------------------------------------------------------------- /tests/s6.nix: -------------------------------------------------------------------------------- 1 | # Run init scripts in the builder for testing purposes 2 | 3 | { pkgs, lib }: 4 | 5 | with lib; 6 | 7 | let 8 | makeInit = c: (lib.makeImage c).init; 9 | makeConfig = c: (lib.makeImage c).config; 10 | 11 | # Run init script in background adn redirect its stdout to a file. A 12 | # test script can use this file to do some tests 13 | runS6Test = test: let 14 | env = concatStringsSep "\n" (mapAttrsToList (n: v: "export ${n}=${v}") (attrByPath ["env"] {} test)); 15 | run = pkgs.runCommand "runS6-${test.config.image.name}" { } '' 16 | ${env} 17 | 18 | echo "Running ${makeInit test.config}"... 19 | ${makeInit test.config} s6-state > s6-log & 20 | S6PID=$! 21 | tail -f s6-log & 22 | 23 | for i in `seq 1 10`; 24 | do 25 | if ${pkgs.writeScript "runS6-testscript" test.testScript} s6-log 26 | then 27 | mkdir $out 28 | cp s6-log $out/ 29 | echo "ok" > $out/result 30 | exit 0 31 | fi 32 | 33 | # If s6 is down, the test fails 34 | if ! ${pkgs.procps}/bin/ps -p $S6PID > /dev/null; 35 | then 36 | echo "Test fails and s6-svscan is down." 37 | exit 1 38 | fi 39 | 40 | sleep 1 41 | done 42 | 43 | # If the timeout is reached, the test fails 44 | echo "Test timeout." 45 | exit 1 46 | ''; 47 | in 48 | # We add the config attribute for debugging 49 | run // { config = makeConfig (test.config); }; 50 | 51 | in 52 | pkgs.lib.mapAttrs (n: v: runS6Test v) { 53 | 54 | # If a long run service with restart = no fails, s6-svscan 55 | # terminates 56 | stopIfLongrunNoRestartFails = { 57 | config = { 58 | image.name = "stopIfLongrunNoRestartFails"; 59 | systemd.services.example.script = '' 60 | exit 1 61 | ''; 62 | }; 63 | testScript = '' 64 | #!${pkgs.stdenv.shell} 65 | grep -q "init stage 3" $1 66 | ''; 67 | }; 68 | 69 | # If a long run service with restart = always fails, the service is 70 | # restarted 71 | stopIfLongrunRestart = { 72 | config = { 73 | image.name = "stopIfLongrunRestart"; 74 | systemd.services.example = { 75 | script = '' 76 | echo "restart" 77 | exit 1 78 | ''; 79 | serviceConfig.Restart = "always"; 80 | }; 81 | }; 82 | testScript = '' 83 | #!${pkgs.stdenv.shell} -e 84 | [ `grep "restart" $1 | wc -l` -ge 2 ] 85 | ''; 86 | }; 87 | 88 | # If a oneshot fails, s6-svscan terminates 89 | stopIfOneshotFail = { 90 | config = { 91 | image.name = "stopIfOneshotFail"; 92 | systemd.services.example = { 93 | script = '' 94 | echo "restart" 95 | exit 1 96 | ''; 97 | serviceConfig.Type = "oneshot"; 98 | }; 99 | }; 100 | testScript = '' 101 | #!${pkgs.stdenv.shell} 102 | grep -q "init stage 3" $1 103 | ''; 104 | }; 105 | 106 | # Oneshot service can have dependencies 107 | dependentOneshot = { 108 | config = { 109 | image.name = "dependentOneshot"; 110 | systemd.services.example-1 = { 111 | script = "echo example-1: MUSTNOTEXISTELSEWHERE_1"; 112 | after = [ "example-2.service" ]; 113 | serviceConfig.Type = "oneshot"; 114 | }; 115 | systemd.services.example-2 = { 116 | script = "echo example-2: MUSTNOTEXISTELSEWHERE_2"; 117 | serviceConfig.Type = "oneshot"; 118 | }; 119 | }; 120 | testScript = '' 121 | #!${pkgs.stdenv.shell} 122 | set -e 123 | grep -q MUSTNOTEXISTELSEWHERE_1 $1 124 | grep -q MUSTNOTEXISTELSEWHERE_2 $1 125 | grep MUSTNOTEXISTELSEWHERE $1 | sort --check --reverse 126 | ''; 127 | }; 128 | 129 | # OneshotPost service can have dependencies 130 | # example-1 is executed after example-2 131 | oneshotPost = { 132 | config = { 133 | image.name = "oneshotPost"; 134 | 135 | systemd.services.example-1 = { 136 | script = "sleep 2; echo example-1: MUSTNOTEXISTELSEWHERE_1"; 137 | after = [ "example-2.service" ]; 138 | serviceConfig.Type = "oneshot"; 139 | }; 140 | systemd.services.example-2 = { 141 | script = "echo example-2: MUSTNOTEXISTELSEWHERE_2"; 142 | }; 143 | }; 144 | testScript = '' 145 | #!${pkgs.stdenv.shell} 146 | set -e 147 | grep -q MUSTNOTEXISTELSEWHERE_1 $1 148 | grep -q MUSTNOTEXISTELSEWHERE_2 $1 149 | grep MUSTNOTEXISTELSEWHERE $1 | head -n2 | sort --check --reverse 150 | ''; 151 | }; 152 | 153 | path = { 154 | config = { 155 | image.name = "path"; 156 | 157 | systemd.services.path = { 158 | script = "hello"; 159 | path = [ pkgs.hello ]; 160 | }; 161 | }; 162 | testScript = '' 163 | #!${pkgs.stdenv.shell} -e 164 | grep -q 'Hello, world!' $1 165 | ''; 166 | }; 167 | 168 | # Environment variables are propagated to the init script 169 | propagatedEnv = { 170 | config = { 171 | image.name = "propagatedEnv"; 172 | systemd.services.exemple.script = "echo $IN_S6_INIT_TEST"; 173 | }; 174 | testScript = '' 175 | #!${pkgs.stdenv.shell} -e 176 | grep -q '^1$' $1 177 | ''; 178 | env = { IN_S6_INIT_TEST = "1"; }; 179 | }; 180 | 181 | # Service environment variables are available 182 | env = { 183 | config = { 184 | image.name = "env"; 185 | s6.services.exemple = { 186 | environment = { "TEST_ENV" = "1"; }; 187 | script = "echo $TEST_ENV"; 188 | }; 189 | }; 190 | testScript = '' 191 | #!${pkgs.stdenv.shell} -e 192 | grep -q '^1$' $1 193 | ''; 194 | }; 195 | 196 | # The special environment variable DEBUG_S6_DONT_KILL_ON_ERROR can 197 | # be used to not kill container when a oneshot fails 198 | s6DontTerminateOnError = { 199 | config = { 200 | image.name = "debugS6DontKillOnError"; 201 | systemd.services.fail = { 202 | script = "exit 1"; 203 | serviceConfig.Type = "oneshot"; 204 | }; 205 | s6.services.longRun = { 206 | script = "exit 2"; 207 | }; 208 | }; 209 | testScript = '' 210 | #!${pkgs.stdenv.shell} -e 211 | sleep 5 212 | ! grep -q "finish" $1 213 | ''; 214 | env = { S6_DONT_TERMINATE_ON_ERROR = "1"; }; 215 | }; 216 | 217 | s6SimpleService = { 218 | config = { 219 | image.name = "s6SimpleService"; 220 | s6.services.simple = { 221 | execStart = "${pkgs.hello}/bin/hello"; 222 | }; 223 | }; 224 | testScript = '' 225 | #!${pkgs.stdenv.shell} -e 226 | grep -q Hello $1 227 | ''; 228 | }; 229 | 230 | # Prestart is executed before ExecStart 231 | preStart = { 232 | config = { 233 | image.name = "preStart"; 234 | systemd.services.example = { 235 | preStart = "echo MUSTNOTEXISTELSEWHERE_1"; 236 | script = '' 237 | echo MUSTNOTEXISTELSEWHERE_2 238 | ''; 239 | }; 240 | }; 241 | testScript = '' 242 | #!${pkgs.stdenv.shell} -e 243 | grep -q MUSTNOTEXISTELSEWHERE_1 $1 244 | grep -q MUSTNOTEXISTELSEWHERE_2 $1 245 | grep MUSTNOTEXISTELSEWHERE $1 | head -n2 | sort --check 246 | ''; 247 | }; 248 | 249 | workingDirectory = { 250 | config = { 251 | image.name = "workingDirectory"; 252 | s6.services.example = { 253 | workingDirectory = "/tmp"; 254 | script = '' 255 | echo $PWD 256 | ''; 257 | }; 258 | }; 259 | testScript = '' 260 | #!${pkgs.stdenv.shell} -e 261 | grep -q /tmp $1 262 | ''; 263 | }; 264 | 265 | oneshotPre = { 266 | config = { 267 | image.name = "oneshotPre"; 268 | s6.services.pre = { 269 | type = "oneshot-pre"; 270 | script = '' 271 | echo oneshot-pre 272 | ''; 273 | }; 274 | }; 275 | testScript = '' 276 | #!${pkgs.stdenv.shell} -e 277 | grep -q oneshot-pre $1 278 | ''; 279 | }; 280 | 281 | logger = { 282 | config = { 283 | image.name = "logger"; 284 | s6.services.logger = { 285 | execLogger = ''${pkgs.gnused}/bin/sed -u "s/^/prefix - /"''; 286 | script = "echo log line"; 287 | restartOnFailure = true; 288 | }; 289 | }; 290 | testScript = '' 291 | #!${pkgs.stdenv.shell} -e 292 | grep -q "prefix - log line" $1 293 | ''; 294 | }; 295 | 296 | preservePATH = { 297 | config = { 298 | image.name = "preservePATH"; 299 | s6.services.preservePATH = { 300 | script = "echo $PATH"; 301 | }; 302 | }; 303 | testScript = '' 304 | #!${pkgs.stdenv.shell} -e 305 | grep -q SHOULD_PRESERVE_INHERITED_PATH $1 306 | ''; 307 | env = { PATH = "$PATH:SHOULD_PRESERVE_INHERITED_PATH"; }; 308 | }; 309 | 310 | } 311 | -------------------------------------------------------------------------------- /tests/systemd.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, dockerImages }: 2 | 3 | with lib; 4 | 5 | lib.makeContainerTest { 6 | image = dockerImages.example-systemd; 7 | testScript = '' 8 | $machine->waitForUnit("docker.service"); 9 | $machine->succeed("docker load -i ${dockerImages.example-systemd}"); 10 | 11 | $machine->succeed("docker run --name systemd -d ${lib.imageRef dockerImages.example-systemd}"); 12 | 13 | # Check the service second is started after the service first since second depend on first 14 | $machine->succeed("docker logs systemd | grep dependent services | sort --check"); 15 | ''; 16 | } 17 | --------------------------------------------------------------------------------