├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── nix-kubernetes ├── default.nix ├── examples └── trivial.nix ├── gulpfile.js ├── lib ├── cli.js ├── index.js ├── models │ ├── Config.js │ ├── Deployment.js │ ├── Namespace.js │ └── resource.js ├── services │ ├── Builder.js │ ├── Deployer.js │ ├── GarbageCollector.js │ ├── JobRunner.js │ └── Kubectl.js └── util.js ├── nix ├── default.nix ├── lib.nix └── options.nix ├── options.md ├── package.json ├── test ├── configmap.nix ├── crd.nix ├── daemonset.nix ├── deployment.nix ├── gc.nix ├── job.nix ├── netorkpolicy.nix ├── pdb.nix ├── pod.nix ├── pv.nix ├── referencing.nix ├── roles.nix ├── scheduledJob.nix ├── secret.nix ├── service.nix ├── serviceAccount.nix ├── statefulset.nix └── storageclass.nix ├── yarn.lock └── yarn.nix /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | result 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v5 4 | - v4 5 | - '0.12' 6 | - '0.10' 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jaka Hudoklin (https://x-truder.net) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This repository is deprecated, please use https://github.com/xtruder/kubenix** 2 | 3 | # nix-kubernetes [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] 4 | > Kubernetes deployment manager written in nix 5 | 6 | ## About 7 | 8 | Nix-kubernetes is deployment manager for kubernetes that users nixos module 9 | system to do declarative kubernetes configuration and deployment. By combining 10 | nix package manager and nix-kubernetes declarative cluster configuration it can 11 | do fully deterministic deployments from packages to configuration. 12 | 13 | ## Depencies 14 | 15 | - nix (for installation instructions go to https://nixos.org/nix/) 16 | 17 | ## Installation 18 | 19 | ```sh 20 | $ nix-env -f https://github.com/xtruder/nix-kubernetes/archive/v0.25.1.tar.gz -iA package 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```bash 26 | Usage: bin/nix-kubernetes [options] 27 | 28 | Commands: 29 | create Create deployment 30 | list List all known deployments 31 | delete Delete deployment 32 | describe Describes deployment 33 | deploy Deploy configuration 34 | config Manage deployment 35 | run-job Run distributed kubernetes job 36 | gc Garbage collect resources (will only gc nix-kubernetes resources) 37 | 38 | Options: 39 | -c, --config path to configuration file [default: "~/.kube/nix-kubernetes.json"] 40 | -h, --help Show help [boolean] 41 | 42 | Examples: 43 | bin/nix-kubernetes create -d name deploy.nix create deployment 44 | bin/nix-kubernetes deploy -d name -n namespace --context my-cluster deploy resources 45 | bin/nix-kubernetes deploy -d name --build-only only build 46 | ``` 47 | 48 | **Deploy** 49 | 50 | ```bash 51 | nix-kubernetes deploy 52 | 53 | Options: 54 | -c, --config path to configuration file [default: "~/.kube/nix-kubernetes.json"] 55 | -h, --help Show help [boolean] 56 | -f, --file Use prebuild deployment file 57 | -d, --deployment Deployment name 58 | -n, --namespace Namespace to deploy 59 | -i, --include Resources to include in deployment 60 | -o, --output Output generated config to file 61 | --context Kubernetes config context to use for deployment 62 | --build-only only build 63 | --dry-run dry run 64 | --restart restart replication controllers 65 | --rolling-update do a rolling update of a controller 66 | --gc garbage collect resources (will only gc nix-kubernetes resources) 67 | 68 | Examples: 69 | nix-kubernetes deploy -d deployment -n namespace -i replicationcontrollers/gitlab --gc --context my-cluster 70 | nix-kubernetes deploy -d deployment -n namespace -i services/mysql --dry-run 71 | ``` 72 | 73 | ## Example usage 74 | 75 | ```sh 76 | $ nix-kubernetes create -d gatehub deploy.nix 77 | $ export NIX_PATH="services=/home/offlinehacker/projects/x-truder.net/services:$NIX_PATH" 78 | $ nix-kubernetes deploy -d gatehub --build-only 79 | $ nix-kubernetes deploy -d gatehub -n namespace --context my-cluster --gc 80 | ``` 81 | 82 | ## Example deployment configurations 83 | 84 | look into examples folder with example deployment configurations. 85 | 86 | ## Options 87 | 88 | For a list of all options look into [options.md](options.md) 89 | 90 | ## Nix-kubernetes services 91 | 92 | Repo with bundle of nix-kubernetes 93 | services is avalible on [https://github.com/x-truder/services](https://github.com/x-truder/services). 94 | 95 | ## Development 96 | 97 | ### Use development version 98 | 99 | ```sh 100 | $ npm link 101 | $ export PATH=~/.npm/bin:$PATH 102 | ``` 103 | 104 | ## Build deployments 105 | 106 | ```sh 107 | $ nix-build -A staging --arg configuration deploy.nix 108 | ``` 109 | 110 | ## License 111 | 112 | MIT © [Jaka Hudoklin](https://x-truder.net) 113 | 114 | 115 | [npm-image]: https://badge.fury.io/js/nix-kubernetes.svg 116 | [npm-url]: https://npmjs.org/package/nix-kubernetes 117 | [travis-image]: https://travis-ci.org/x-truder/nix-kubernetes.svg?branch=master 118 | [travis-url]: https://travis-ci.org/x-truder/nix-kubernetes 119 | [daviddm-image]: https://david-dm.org/x-truder/nix-kubernetes.svg?theme=shields.io 120 | [daviddm-url]: https://david-dm.org/x-truder/nix-kubernetes 121 | [coveralls-image]: https://coveralls.io/repos/x-truder/nix-kubernetes/badge.svg 122 | [coveralls-url]: https://coveralls.io/r/x-truder/nix-kubernetes 123 | -------------------------------------------------------------------------------- /bin/nix-kubernetes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli'); 4 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | 4 | options = (import { 5 | modules = [ (import ./nix/options.nix) ]; 6 | }).options; 7 | optionsList = with pkgs.lib; 8 | filter (opt: opt.visible && !opt.internal) (optionAttrSetToDocList options); 9 | # Replace functions by the string 10 | substFunction = with pkgs.lib; x: 11 | if builtins.isAttrs x then mapAttrs (name: substFunction) x 12 | else if builtins.isList x then map substFunction x 13 | else if builtins.isFunction x then "" 14 | else x; 15 | optionsList' = with pkgs.lib; flip map optionsList (opt: opt // { 16 | declarations = opt.declarations; 17 | } 18 | // optionalAttrs (opt ? description) { example = substFunction opt.description; } 19 | // optionalAttrs (opt ? example) { example = substFunction opt.example; } 20 | // optionalAttrs (opt ? default) { default = substFunction opt.default; } 21 | // optionalAttrs (opt ? type) { type = substFunction opt.type; }); 22 | 23 | exposedOptionList = with pkgs.lib; (map (o: { 24 | name = o.name; 25 | value = removeAttrs o ["name" "visible" "internal"]; 26 | }) optionsList' 27 | ); 28 | 29 | filterOptions = prefixes: with pkgs.lib; 30 | (filter (v: any (p: hasPrefix p v.name) prefixes) 31 | exposedOptionList 32 | ); 33 | 34 | sections = [ 35 | "namespaces" "pods" "services" "controllers" "deployments" "daemonsets" 36 | "scheduledJobs" "jobs" "ingress" "secrets" "pvc" "roles" "clusterRoles" 37 | "roleBindings" "clusterRoleBindings" "serviceAccounts" "configMaps" 38 | "petSets" "statefulSets" "networkPolicies" "customResources" 39 | "defaults" 40 | ]; 41 | 42 | version = (builtins.fromJSON (builtins.readFile ./package.json)).version; 43 | 44 | in 45 | with pkgs.lib; 46 | with (import (builtins.fetchTarball https://github.com/moretea/yarn2nix/archive/master.tar.gz) { inherit pkgs; }); 47 | { 48 | inherit profiles; 49 | 50 | package = mkYarnPackage { 51 | name = "nix-kubernetes-${version}"; 52 | src = ./.; 53 | packageJson = ./package.json; 54 | yarnLock = ./yarn.lock; 55 | # NOTE: this is optional and generated dynamically if omitted 56 | yarnNix = ./yarn.nix; 57 | }; 58 | 59 | options = pkgs.stdenv.mkDerivation { 60 | name = "options-json"; 61 | 62 | buildCommand = '' 63 | mkdir -p $out 64 | cp ${pkgs.writeText "options.json" (let 65 | mkSection = prefix: concatMapStringsSep "\n" (v: '' 66 | * `${v.name}`: 67 | 68 | ${v.value.description or "No description"} 69 | 70 | **Default:** ${builtins.toJSON v.value.default or "..."} 71 | **Example:** ${if v.value.description == v.value.example then "..." else (builtins.toJSON v.value.example)} 72 | '') (filterOptions [prefix]); 73 | in '' 74 | # nix-kubernetes options 75 | 76 | List of all nix-kubernetes options 77 | 78 | '' + (concatMapStringsSep "\n" (name: 79 | "## ${name} options\n\n" + 80 | (mkSection "kubernetes.${name}") 81 | ) sections))} $out/options.md 82 | ''; # */ 83 | 84 | meta.description = "List of NixOS options in markdown format"; 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /examples/trivial.nix: -------------------------------------------------------------------------------- 1 | { 2 | default = { 3 | kubernetes.namespace.name = "default"; 4 | 5 | # Elasticserach replication controller 6 | kubernetes.controllers.elasticsearch = { 7 | dependencies = ["services/elasticsearch" "pvc/elasticsearch"]; 8 | pod.containers.elasticsearch = { 9 | image = "quay.io/pires/docker-elasticsearch-kubernetes:1.7.2"; 10 | env = { 11 | NAMESPACE = "default"; 12 | CLUSTER_NAME = "my_cluster"; 13 | NODE_MASTER = "true"; 14 | NODE_DATA = "true"; 15 | HTTP_ENABLE = "true"; 16 | }; 17 | ports = [{ port = 9200; } { port = 9300; }]; 18 | mounts = [{ 19 | name = "storage"; 20 | mountPath = "/data"; 21 | }]; 22 | security.capabilities.add = ["IPC_LOCK"]; 23 | }; 24 | 25 | pod.volumes.storage = { 26 | type = "persistentVolumeClaim"; 27 | options.claimName = "elasticsearch"; 28 | }; 29 | }; 30 | 31 | # Elasticsearch load balancer 32 | kubernetes.services.elasticsearch.ports = [{ port = 9200; }]; 33 | 34 | # Elasticsearch persistent volume claim 35 | kubernetes.pvc.elasticsearch.size = "1G"; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var gulp = require('gulp'); 4 | var eslint = require('gulp-eslint'); 5 | var excludeGitignore = require('gulp-exclude-gitignore'); 6 | var mocha = require('gulp-mocha'); 7 | var istanbul = require('gulp-istanbul'); 8 | var plumber = require('gulp-plumber'); 9 | var coveralls = require('gulp-coveralls'); 10 | 11 | gulp.task('static', function () { 12 | return gulp.src('**/*.js') 13 | .pipe(excludeGitignore()) 14 | .pipe(eslint()) 15 | .pipe(eslint.format()) 16 | .pipe(eslint.failAfterError()); 17 | }); 18 | 19 | gulp.task('pre-test', function () { 20 | return gulp.src('lib/**/*.js') 21 | .pipe(excludeGitignore()) 22 | .pipe(istanbul({ 23 | includeUntested: true 24 | })) 25 | .pipe(istanbul.hookRequire()); 26 | }); 27 | 28 | gulp.task('test', ['pre-test'], function (cb) { 29 | var mochaErr; 30 | 31 | gulp.src('test/**/*.js') 32 | .pipe(plumber()) 33 | .pipe(mocha({reporter: 'spec'})) 34 | .on('error', function (err) { 35 | mochaErr = err; 36 | }) 37 | .pipe(istanbul.writeReports()) 38 | .on('end', function () { 39 | cb(mochaErr); 40 | }); 41 | }); 42 | 43 | gulp.task('watch', function () { 44 | gulp.watch(['lib/**/*.js', 'test/**'], ['test']); 45 | }); 46 | 47 | gulp.task('coveralls', ['test'], function () { 48 | if (!process.env.CI) { 49 | return; 50 | } 51 | 52 | return gulp.src(path.join(__dirname, 'coverage/lcov.info')) 53 | .pipe(coveralls()); 54 | }); 55 | 56 | gulp.task('default', ['static', 'test', 'coveralls']); 57 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const yargs = require('yargs'); 4 | const _ = require('lodash'); 5 | const Table = require('cli-table2'); 6 | const YAML = require('yamljs'); 7 | 8 | const Util = require('./util'); 9 | const Config = require('./models/Config'); 10 | const Deployment = require('./models/Deployment'); 11 | const Builder = require('./services/Builder'); 12 | const Deployer = require('./services/Deployer'); 13 | const JobRunner = require('./services/JobRunner'); 14 | const GarbageCollector = require('./services/GarbageCollector'); 15 | const Kubectl = require('./services/Kubectl'); 16 | 17 | const args = process.argv.join(' '); 18 | const append = _.first(_.at(args.split(" -- "), 1)); 19 | 20 | function parseArgs(args) { 21 | const result = {}; 22 | 23 | _.forEach(args, arg => { 24 | const key = _.first(arg.split('=')); 25 | const value = _.last(arg.split('=')) || true; 26 | 27 | _.set(result, key, value); 28 | }); 29 | 30 | return result; 31 | } 32 | 33 | yargs // eslint-disable-line no-unused-expressions 34 | .usage('Usage: $0 [options]') 35 | .default('c', '~/.kube/nix-kubernetes.json') 36 | .alias('c', 'config').global('c') 37 | .describe('c', 'path to configuration file') 38 | .command('create ', 'Create deployment', yargs => { 39 | return yargs 40 | .demand('d') 41 | .alias('d', 'deployment') 42 | .describe('d', 'Deployment name') 43 | .example('$0 create -n deployment deploy.nix', 'create a new deployment'); 44 | }, argv => { 45 | const fullPath = Util.resolvePath(argv.config); 46 | const config = Config.loadOrCreate(fullPath); 47 | 48 | if (_.has(config.deployments, argv.deployment)) { 49 | console.error('Deployment with this name already exists'); 50 | process.exit(1); 51 | } 52 | 53 | config.updateOrCreate(argv.deployment, argv.file); 54 | config.save(fullPath); 55 | 56 | console.log('Deployment created'); 57 | }) 58 | .command('list', 'List all known deployments', _.noop, argv => { 59 | const fullPath = Util.resolvePath(argv.config); 60 | const config = Config.loadOrCreate(fullPath); 61 | 62 | const table = new Table({head: ['name', 'path']}); 63 | _.forEach(config.deployments, deployment => { 64 | table.push([deployment.name, deployment.file]); 65 | }); 66 | 67 | console.log(table.toString()); 68 | }) 69 | .command('delete', 'Delete deployment', yargs => { 70 | return yargs 71 | .demand('d') 72 | .alias('d', 'deployment') 73 | .describe('d', 'Deployment name') 74 | .example('$0 delete -d deployment', 'delete deployment'); 75 | }, argv => { 76 | const fullPath = Util.resolvePath(argv.config); 77 | const config = Config.loadOrCreate(fullPath); 78 | 79 | if (!_.has(config.deployments, argv.deployment)) { 80 | console.error('Deployment with this name does not exist'); 81 | process.exit(1); 82 | } 83 | 84 | config.delete(argv.deployment); 85 | config.save(fullPath); 86 | 87 | console.log('Deployment deleted'); 88 | }) 89 | .command('describe', 'Describes deployment', yargs => { 90 | return yargs 91 | .demand('d') 92 | .alias('d', 'deployment') 93 | .describe('d', 'Deployment name') 94 | .example('$0 describe -d deployment', 'describe deployment'); 95 | }, argv => { 96 | const fullPath = Util.resolvePath(argv.config); 97 | const config = Config.loadOrCreate(fullPath); 98 | 99 | if (!_.has(config.deployments, argv.deployment)) { 100 | console.error('Deployment with this name does not exist'); 101 | process.exit(1); 102 | } 103 | 104 | const deployment = config.deployments[argv.deployment]; 105 | 106 | const builder = new Builder(deployment); 107 | builder.eval().then(deployment => { 108 | console.log(YAML.stringify(deployment.toJSON(), 4)); 109 | }); 110 | }) 111 | .command('deploy', 'Deploy configuration', yargs => { 112 | return yargs 113 | .describe('f', 'Use prebuild deployment file') 114 | .alias('f', 'file') 115 | .describe('d', 'Deployment name') 116 | .alias('d', 'deployment') 117 | .describe('n', 'Namespace to deploy') 118 | .alias('n', 'namespace') 119 | .describe('i', 'Resources to include in deployment') 120 | .alias('i', 'include') 121 | .describe('o', 'Output generated config to file') 122 | .alias('o', 'output') 123 | .describe('context', 'Kubernetes config context to use for deployment') 124 | .describe('build-only', 'only build') 125 | .describe('dry-run', 'dry run') 126 | .describe('restart', 'restart replication controllers') 127 | .describe('rolling-update', 'do a rolling update of a controller') 128 | .describe('gc', 'garbage collect resources (will only gc nix-kubernetes resources)') 129 | .example('$0 deploy -d deployment -n namespace -i replicationcontrollers/gitlab --gc --context my-cluster') 130 | .example('$0 deploy -d deployment -n namespace -i services/mysql --dry-run'); 131 | }, argv => { 132 | const fullPath = Util.resolvePath(argv.config); 133 | const config = Config.loadOrCreate(fullPath); 134 | 135 | let deployment; 136 | let evaledDeployment; 137 | if (argv.file) { 138 | deployment = new Deployment('local'); 139 | deployment.loadSpec(Util.resolvePath(argv.file)); 140 | 141 | evaledDeployment = Promise.resolve(deployment); 142 | } else { 143 | if (!_.has(config.deployments, argv.deployment)) { 144 | console.error('Deployment with this name does not exist'); 145 | process.exit(1); 146 | } 147 | 148 | deployment = config.deployments[argv.deployment]; 149 | 150 | const builder = new Builder(deployment); 151 | evaledDeployment = builder.eval(); 152 | } 153 | 154 | evaledDeployment.then(deployment => { 155 | console.log('Config:', deployment.specPath); 156 | 157 | if (argv.output) { 158 | const outputPath = Util.resolvePath(argv.output); 159 | deployment.writeSpec(outputPath); 160 | } 161 | 162 | if (!argv["build-only"]) { 163 | if (!_.has(deployment.namespaces, argv.namespace)) { 164 | console.error('Namespace with this name does not exist'); 165 | process.exit(1); 166 | } 167 | 168 | if (argv.context) { 169 | console.log("Using context:", argv.context); 170 | } 171 | 172 | const kubectl = new Kubectl({ 173 | context: argv.context, 174 | dryRun: argv['dry-run'], 175 | append: append 176 | }); 177 | const deployer = new Deployer(deployment.namespaces[argv.namespace], kubectl); 178 | const options = { 179 | restart: argv.restart, 180 | rollingUpdate: argv['rolling-update'] 181 | }; 182 | 183 | return deployer.deploy(argv.include, options).then(() => { 184 | console.log(`deployment complete${argv['dry-run'] ? " (dry-run)" : ""}`); 185 | }).catch(err => { 186 | console.error('error with deployment', err.toString()); 187 | }).then(() => { 188 | if (argv.gc) { 189 | const garbageCollector = 190 | new GarbageCollector(deployment.namespaces[argv.namespace], kubectl); 191 | 192 | return garbageCollector.run(); 193 | } 194 | }); 195 | } 196 | }); 197 | }) 198 | .command('config', 'Manage deployment', yargs => { 199 | return yargs 200 | .describe('d', 'Deployment name') 201 | .alias('d', 'deployment') 202 | .demand('d') 203 | .describe('s', 'set argument') 204 | .alias('s', 'set-arg') 205 | .example('$0 args -d deployment --set key=value'); 206 | }, argv => { 207 | const fullPath = Util.resolvePath(argv.config); 208 | const config = Config.loadOrCreate(fullPath); 209 | 210 | if (!_.has(config.deployments, argv.deployment)) { 211 | console.error('Deployment with this name does not exist'); 212 | process.exit(1); 213 | } 214 | 215 | const deployment = config.deployments[argv.deployment]; 216 | 217 | if (argv.setArg) { 218 | const args = parseArgs(_.isArray(argv.setArg) ? argv.setArg : [argv.setArg]); 219 | _.forEach(args, (value, key) => { 220 | if (value === '-') { 221 | deployment.unsetArg(key); 222 | } else { 223 | deployment.setArg(key, value); 224 | } 225 | }); 226 | } 227 | 228 | config.save(fullPath); 229 | }) 230 | .command('run-job ', 'Run distributed kubernetes job', yargs => { 231 | return yargs 232 | .describe('d', 'Deployment name') 233 | .alias('d', 'deployment') 234 | .demand('d') 235 | .describe('n', 'Namespace where job is defined') 236 | .alias('n', 'namespace') 237 | .describe('wait', 'Wait for job completion') 238 | .describe('gc', 'Garbage collect completed job') 239 | .implies('gc', 'wait') 240 | .describe('a', 'set argument') 241 | .alias('a', 'arg') 242 | .describe('dry-run', 'dry run') 243 | .example('$0 run-job -d deployment -n namespace jobname --arg key=value'); 244 | }, argv => { 245 | const fullPath = Util.resolvePath(argv.config); 246 | const config = Config.loadOrCreate(fullPath); 247 | 248 | if (!_.has(config.deployments, argv.deployment)) { 249 | console.error('Deployment with this name does not exist'); 250 | process.exit(1); 251 | } 252 | 253 | let args = {}; 254 | if (_.isArray(argv.arg)) { 255 | args = parseArgs(argv.arg); 256 | } else if (argv.arg) { 257 | args = parseArgs([argv.arg]); 258 | } 259 | 260 | const deployment = config.deployments[argv.deployment]; 261 | const builder = new Builder(deployment); 262 | builder.eval(args).then(config => { 263 | console.log('Config:', deployment.specPath); 264 | 265 | if (!_.has(config.namespaces, argv.namespace)) { 266 | console.error('Namespace with this name does not exist'); 267 | process.exit(1); 268 | } 269 | 270 | if (argv.context) { 271 | console.log("Using context:", argv.context); 272 | } 273 | 274 | const kubectl = new Kubectl({context: argv.context, dryRun: argv['dry-run']}); 275 | const jobRunner = new JobRunner(config.namespaces[argv.namespace], kubectl); 276 | jobRunner.run(argv.name, {wait: argv.wait, gc: argv.gc}).then(() => { 277 | console.log('job complete', argv.name); 278 | }).catch(err => { 279 | console.log('error with job processing', err.toString()); 280 | }); 281 | }); 282 | }) 283 | .command('gc', 'Garbage collect resources (will only gc nix-kubernetes resources)', yargs => { 284 | return yargs 285 | .demand('d') 286 | .alias('d', 'deployment') 287 | .describe('d', 'Deployment name') 288 | .describe('n', 'Namespace to be garbage collected') 289 | .alias('n', 'namespace') 290 | .describe('dry-run', 'dry run') 291 | .example('$0 describe -d deployment', 'describe deployment'); 292 | }, argv => { 293 | const fullPath = Util.resolvePath(argv.config); 294 | const config = Config.loadOrCreate(fullPath); 295 | 296 | if (!_.has(config.deployments, argv.deployment)) { 297 | console.error('Deployment with this name does not exist'); 298 | process.exit(1); 299 | } 300 | 301 | const deployment = config.deployments[argv.deployment]; 302 | 303 | const builder = new Builder(deployment); 304 | builder.eval().then(deployment => { 305 | if (!_.has(deployment.namespaces, argv.namespace)) { 306 | console.error('Namespace with this name does not exist'); 307 | process.exit(1); 308 | } 309 | 310 | const kubectl = new Kubectl({ 311 | context: argv.context, 312 | dryRun: argv['dry-run'], 313 | append: append 314 | }); 315 | 316 | const garbageCollector = new GarbageCollector(deployment.namespaces[argv.namespace], kubectl); 317 | return garbageCollector.run().catch(err => { 318 | console.error('error with garbage collection', err.toString()); 319 | }); 320 | }); 321 | }) 322 | .demand(1) 323 | .help('h').alias('h', 'help') 324 | .example('$0 create -d name deploy.nix', 'create deployment') 325 | .example('$0 deploy -d name -n namespace --context my-cluster', 'deploy resources') 326 | .example('$0 deploy -d name --build-only', 'only build') 327 | .wrap(null) 328 | .argv; 329 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /lib/models/Config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const fs = require('fs'); 5 | 6 | const Deployment = require('./Deployment'); 7 | const Util = require('../util'); 8 | 9 | class Config { 10 | constructor(data) { 11 | this.data = _.defaults(data, { 12 | deployments: {} 13 | }); 14 | } 15 | 16 | get deployments() { 17 | return _.mapValues( 18 | this.data.deployments, 19 | (deployment, name) => new Deployment(name, deployment) 20 | ); 21 | } 22 | 23 | updateOrCreate(name, file) { 24 | this.data.deployments[name] = { 25 | file: Util.resolvePath(file) 26 | }; 27 | } 28 | 29 | delete(name) { 30 | delete this.data.deployments[name]; 31 | } 32 | 33 | save(path) { 34 | fs.writeFileSync(path, JSON.stringify(this.data), 'utf8'); 35 | } 36 | 37 | static loadOrCreate(path) { 38 | if (fs.existsSync(path)) { 39 | const content = JSON.parse(fs.readFileSync(path, 'utf8')); 40 | return new Config(content); 41 | } 42 | 43 | return new Config({}); 44 | } 45 | } 46 | 47 | module.exports = Config; 48 | -------------------------------------------------------------------------------- /lib/models/Deployment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const _ = require('lodash'); 5 | 6 | const Namespace = require('./Namespace'); 7 | 8 | /** 9 | * Deploy model specifications 10 | */ 11 | class Deployment { 12 | constructor(name, data = {}) { 13 | this.name = name; 14 | this.data = _.defaults(data, { 15 | args: {} 16 | }); 17 | } 18 | 19 | get args() { 20 | return this.data.args; 21 | } 22 | 23 | setArg(key, value) { 24 | this.data.args[key] = value; 25 | } 26 | 27 | unsetArg(key) { 28 | delete this.data.args[key]; 29 | } 30 | 31 | get file() { 32 | return this.data.file; 33 | } 34 | 35 | /** 36 | * Gets namespaces to deploy to 37 | * 38 | * @return {Object.} Namespaces 39 | */ 40 | get namespaces() { 41 | return _.mapValues(this.spec, deployment => new Namespace(deployment)); 42 | } 43 | 44 | loadSpec(path) { 45 | const parsedSpec = JSON.parse(fs.readFileSync(path)); 46 | this.spec = parsedSpec; 47 | this.specPath = path; 48 | } 49 | 50 | writeSpec(path) { 51 | const serializedSpec = JSON.stringify(this.spec); 52 | fs.writeFileSync(path, serializedSpec); 53 | } 54 | 55 | toJSON() { 56 | const result = { 57 | name: this.name, 58 | file: this.file, 59 | args: this.args 60 | }; 61 | 62 | if (this.spec) { 63 | result.specPath = this.specPath; 64 | } 65 | 66 | return result; 67 | } 68 | } 69 | 70 | module.exports = Deployment; 71 | -------------------------------------------------------------------------------- /lib/models/Namespace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const Resource = require('./resource'); 6 | 7 | /** 8 | * Namespace model specifications 9 | */ 10 | class Namespace { 11 | constructor(data) { 12 | this.data = data; 13 | } 14 | 15 | /** 16 | * Gets resources 17 | * 18 | * @return {Object.} Mapping betwene type/name : resource 19 | */ 20 | get resources() { 21 | // Transform entries in type/name : resource 22 | return _.transform(this.data.resources, (result, element, type) => { 23 | _.forEach(element, (resource, name) => result.push(type + '/' + name)); 24 | }, []); 25 | } 26 | /** 27 | * Gets list of namespaces to garbage collect 28 | * 29 | * @return {Array} Gets namespaces to garbage collect 30 | */ 31 | get gcNamespaces() { 32 | return this.data.gcNamespaces; 33 | } 34 | 35 | /** 36 | * Gets resource by path 37 | * 38 | * @param path string Resource path in format type/name 39 | * @return {Resource} Resource identifed by path 40 | */ 41 | getResource(path) { 42 | const type = path.split('/')[0]; 43 | const name = path.split('/')[1]; 44 | 45 | const resource = _.get(this.data.resources, [type, name]); 46 | 47 | return resource ? new Resource(resource) : undefined; 48 | } 49 | 50 | /** 51 | * Gets job by name 52 | * 53 | * @param name string Name of the job 54 | * @return {Object} job kubernetes resource 55 | */ 56 | getJob(name) { 57 | return _.get(this.data.jobs, name); 58 | } 59 | } 60 | 61 | module.exports = Namespace; 62 | -------------------------------------------------------------------------------- /lib/models/resource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | class Resource { 6 | constructor(data) { 7 | this.data = data; 8 | } 9 | 10 | get kind() { 11 | return _.get(this.data, 'kind'); 12 | } 13 | 14 | get name() { 15 | return _.get(this.data, 'metadata.name'); 16 | } 17 | 18 | get namespace() { 19 | return _.get(this.data, 'metadata.namespace'); 20 | } 21 | 22 | get value() { 23 | return this.data; 24 | } 25 | } 26 | 27 | module.exports = Resource; 28 | -------------------------------------------------------------------------------- /lib/services/Builder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const tmp = require('tmp'); 6 | const _ = require('lodash'); 7 | 8 | const Util = require('../util'); 9 | 10 | class Builder { 11 | constructor(deployment) { 12 | this.deployment = deployment; 13 | } 14 | 15 | get workdir() { 16 | return path.resolve(path.join(__dirname, '..', '..', 'nix')); 17 | } 18 | 19 | eval(args) { 20 | args = args || {}; 21 | 22 | const tmpFile = tmp.fileSync(); 23 | fs.write(tmpFile.fd, JSON.stringify(_.extend(this.deployment.args, args))); 24 | return Util.run( 25 | `nix-build --arg configuration ${this.deployment.file} --arg args ${tmpFile.name} --no-out-link ${this.workdir}/default.nix` 26 | ).then(result => { 27 | tmpFile.removeCallback(); 28 | 29 | // remove new lines from file 30 | const path = _.trimEnd(result); 31 | 32 | this.deployment.loadSpec(path); 33 | 34 | // construct new specs model 35 | return this.deployment; 36 | }); 37 | } 38 | } 39 | 40 | module.exports = Builder; 41 | -------------------------------------------------------------------------------- /lib/services/Deployer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Promise = require('bluebird'); 5 | const assert = require('hoek').assert; 6 | 7 | class Deployer { 8 | constructor(namespace, kubectl) { 9 | this.namespace = namespace; 10 | this.kubectl = kubectl; 11 | } 12 | 13 | deployResource(path, options, context) { 14 | if (context.deployed.has(path)) { 15 | return Promise.resolve(); 16 | } 17 | 18 | let requirements = Promise.resolve(); 19 | const resource = this.namespace.getResource(path); 20 | 21 | assert(resource, `Resource not defined: ${path}`); 22 | 23 | const dependencies = _.get(resource.value, ["metadata", "annotations", "x-truder.net/dependencies"]); 24 | if (dependencies) { 25 | requirements = Promise.mapSeries(dependencies.split(','), dep => { 26 | return this.deployResource(dep, options, context); 27 | }); 28 | } 29 | 30 | return requirements.then(() => { 31 | if (options.rollingUpdate && resource.type === 'replicationcontrollers') { 32 | return this.kubectl.rollingUpdate(resource.namespace, options.rollingUpdate, resource.value); 33 | } 34 | 35 | return this.kubectl.deploy(resource.namespace, resource.kind, resource.name, resource.value); 36 | }).then(() => { 37 | if (options.restart && resource.type === 'replicationcontrollers') { 38 | return this.kubectl.deleteByLabels(resource.name, 'pod', resource.value.spec.selector); 39 | } 40 | }).tap(() => { 41 | context.deployed.add(path); 42 | }); 43 | } 44 | 45 | deploy(include, options) { 46 | let resources; 47 | 48 | if (_.isEmpty(include)) { 49 | resources = this.namespace.resources; 50 | } else { 51 | resources = _.isArray(include) ? include : [include]; 52 | } 53 | 54 | let context = { 55 | deployed: new Set() 56 | }; 57 | 58 | return Promise.mapSeries(resources, path => this.deployResource(path, options, context)); 59 | } 60 | } 61 | 62 | module.exports = Deployer; 63 | -------------------------------------------------------------------------------- /lib/services/GarbageCollector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Promise = require('bluebird'); 5 | 6 | class GarbageCollector { 7 | constructor(namespace, kubectl) { 8 | this.namespace = namespace; 9 | this.kubectl = kubectl; 10 | } 11 | 12 | run() { 13 | return this.kubectl.listAllNamespaces(['all']).map(resource => { 14 | const name = `${resource.metadata.namespace}/${resource.kind}/${resource.metadata.name}`; 15 | 16 | if ( 17 | _.includes(resource.metadata.namespace, this.namespace.gcNamespaces) && 18 | !_.find(this.namespace.resources, name => { 19 | const resourceSpec = this.namespace.getResource(name).data; 20 | const result = ( 21 | resource.metadata.annotations['x-truder.net/kind'] === 'nix-kubernetes' && 22 | resource.metadata.annotations['x-truder.net/version'] === 'v1' && 23 | resourceSpec.kind === resource.kind && 24 | resourceSpec.metadata.namespace === resource.metadata.namespace && 25 | resourceSpec.metadata.name === resource.metadata.name 26 | ) || resource.metadata.ownerReferences; 27 | 28 | return result; 29 | }) 30 | ) { 31 | console.log(`deleting resource ${name}`); 32 | return this.kubectl.delete( 33 | resource.metadata.namespace, 34 | resource.kind, 35 | resource.metadata.name 36 | ); 37 | } 38 | 39 | return Promise.resolve(); 40 | }); 41 | } 42 | } 43 | 44 | module.exports = GarbageCollector; 45 | -------------------------------------------------------------------------------- /lib/services/JobRunner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | class JobRunner { 6 | constructor(namespace, kubectl) { 7 | this.namespace = namespace; 8 | this.kubectl = kubectl; 9 | } 10 | 11 | /** 12 | * Run job with a name 13 | * 14 | * @param {string} name Name of the job to run 15 | * @param {object} options Options passed to job runner 16 | * @return {Object.} Namespaces 17 | */ 18 | run(name, options) { 19 | const job = this.namespace.getJob(name); 20 | const id = (Date.now() % 100000000).toString(); 21 | 22 | if (!job) { 23 | return Promise.reject(new Error('Job with that name not defined')); 24 | } 25 | 26 | const namespace = job.metadata.namespace; 27 | 28 | job.metadata.name = job.metadata.name + '-' + id; 29 | job.spec.template.metadata.labels = 30 | _.extend(job.spec.template.metadata.labels, {job: name}); 31 | job.spec.template.metadata.name = job.metadata.name; 32 | 33 | return this.kubectl.deploy(namespace, 'job', job.metadata.name, job).then(() => { 34 | if (options.wait) { 35 | return this.kubectl.wait(namespace, 'pod', `-l job-id=${id}`, {timeout: 100000}, pods => { 36 | const pod = _.first(pods.items); 37 | 38 | if ( 39 | _.get(pod, 'status.phase') === 'Running' || 40 | _.get(pod, 'status.phase') === 'Succeeded' || 41 | _.get(pod, 'status.phase') === 'Failed' 42 | ) { 43 | return pod; 44 | } 45 | 46 | return Promise.reject(new Error("retry")); 47 | }).then(pod => { 48 | const child = this.kubectl.logs(namespace, pod.metadata.name); 49 | child.stdout.pipe(process.stdout); 50 | 51 | return this.kubectl.wait(namespace, 'job', job.metadata.name, {timeout: 100000}, job => { 52 | if (!job.status.succeeded) { 53 | return Promise.reject(); 54 | } 55 | }).then(() => { 56 | child.kill(); 57 | console.log("job complete"); 58 | }); 59 | }).then(() => { 60 | if (options.gc) { 61 | return this.kubectl.delete(namespace, 'job', job.metadata.name); 62 | } 63 | }); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | module.exports = JobRunner; 70 | -------------------------------------------------------------------------------- /lib/services/Kubectl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | const retry = require('bluebird-retry'); 5 | const _ = require('lodash'); 6 | 7 | const Util = require('../util'); 8 | 9 | class Kubectl { 10 | constructor(options) { 11 | this.options = options; 12 | } 13 | 14 | _exec(command, handler) { 15 | if (this.options.dryRun) { 16 | return childProcess.exec(`${command} --dry-run`, handler); 17 | } 18 | return childProcess.exec(command, handler); 19 | } 20 | 21 | _command(args) { 22 | let cmd = ['kubectl']; 23 | 24 | if (this.options.context) { 25 | cmd.push(`--context ${this.options.context}`); 26 | } 27 | 28 | cmd.push(args); 29 | return cmd.join(' '); 30 | } 31 | 32 | exists(ns, type, name) { 33 | return Util.run(this._command(`--namespace ${ns} get ${type} ${name}`)) 34 | .then(Promise.resolve(true)) 35 | .catch(error => { 36 | if (error.toString().indexOf('not found') > -1) { 37 | return Promise.resolve(false); 38 | } 39 | 40 | throw error; 41 | }); 42 | } 43 | 44 | get(ns, type, name) { 45 | return Util.run(this._command(`--namespace ${ns} get -o json ${type} ${name}`)) 46 | .then(result => { 47 | return JSON.parse(result); 48 | }) 49 | .catch(error => { 50 | if (error.toString().indexOf('not found') > -1) { 51 | return Promise.resolve(false); 52 | } 53 | 54 | throw error; 55 | }); 56 | } 57 | 58 | listAllNamespaces(types) { 59 | return Util.run(this._command(`get -o json ${types.join(',')} --all-namespaces`)) 60 | .then(result => JSON.parse(result)) 61 | .then(result => result.items) 62 | .catch(error => { 63 | if (error.toString().indexOf('not found') > -1) { 64 | return Promise.resolve(false); 65 | } 66 | 67 | throw error; 68 | }); 69 | } 70 | 71 | delete(ns, type, name) { 72 | return Util.run(this._command(`--namespace ${ns} delete ${type} ${name}`)); 73 | } 74 | 75 | deleteByLabels(ns, type, labels) { 76 | const selector = _.map(labels, (value, name) => `${name}=${value}`).join(','); 77 | return Util.run(this._command(`--namespace ${ns} delete ${type} -l ${selector}`)); 78 | } 79 | 80 | wait(ns, type, name, options, condition) { 81 | return retry(() => { 82 | return this.get(ns, type, name).then(result => { 83 | return condition(result); 84 | }); 85 | }, options); 86 | } 87 | 88 | deploy(ns, type, name, resource) { 89 | console.log(`deploying${this.options.dryRun ? " (dry-run)" : ""}:`, type, name); 90 | return this.exists(ns, type, name).then(exists => { 91 | return new Promise((res, rej) => { 92 | let child; 93 | let handler = (error, stdout) => { 94 | if (error) { 95 | return rej(error); 96 | } 97 | 98 | res(stdout); 99 | }; 100 | 101 | const append = this.options.append || ''; 102 | 103 | if (exists && type === 'StatefulSet') { 104 | delete resource.spec.volumeClaimTemplates; 105 | } 106 | 107 | child = this._exec(this._command(`--namespace ${ns} apply -f - ${append}`), handler); 108 | 109 | child.stdin.write(JSON.stringify(resource)); 110 | child.stdin.end(); 111 | }); 112 | }); 113 | } 114 | 115 | rollingUpdate(ns, name, resource) { 116 | return new Promise((res, rej) => { 117 | let handler = (error, stdout) => { 118 | if (error) { 119 | return rej(error); 120 | } 121 | 122 | res(stdout); 123 | }; 124 | 125 | const append = this.options.append || ''; 126 | 127 | const child = this._exec(this._command(`--namespace ${ns} rolling-update ${name} -f - ${append}`), handler); 128 | 129 | child.stdout.on('data', data => process.stdout.write(data)); 130 | 131 | child.stdin.write(JSON.stringify(resource)); 132 | child.stdin.end(); 133 | }); 134 | } 135 | 136 | logs(ns, name, handler) { 137 | const child = childProcess.exec(this._command(`--namespace ${ns} logs -f ${name}`), handler); 138 | 139 | return child; 140 | } 141 | } 142 | 143 | module.exports = Kubectl; 144 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const expandTilde = require('expand-tilde'); 3 | const exec = require('child_process').exec; 4 | const Promise = require('bluebird'); 5 | 6 | module.exports = { 7 | resolvePath: p => { 8 | return path.resolve(expandTilde(p)); 9 | }, 10 | 11 | run: command => { 12 | return new Promise((res, rej) => { 13 | exec(command, {maxBuffer: 1024 * 100000}, (error, stdout) => { 14 | if (error) { 15 | return rej(error); 16 | } 17 | 18 | res(stdout); 19 | }); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}, configuration ? ./test.nix, args ? null, ... }: 2 | 3 | with pkgs.lib; 4 | 5 | with import ./lib.nix { inherit (pkgs) lib; inherit pkgs; }; 6 | 7 | let 8 | argsContent = 9 | if args != null 10 | then builtins.fromJSON (builtins.readFile args) 11 | else {}; 12 | 13 | deployments = import configuration; 14 | 15 | # Evaluates deployment 16 | evalDeployment = name: deployment: let 17 | otherDeployments = filterAttrs (name2: _: name != name2) deployments; 18 | in (evalModules { 19 | modules = [./options.nix deployment argsContent]; 20 | args = { 21 | inherit pkgs; 22 | deployments = mapAttrs ( 23 | name: deployment: evalDeployment name deployment 24 | ) otherDeployments; 25 | }; 26 | }); 27 | 28 | result = mapAttrs (name: deployment: let 29 | config = (evalDeployment name deployment).config; 30 | in { 31 | # Deployment resources 32 | resources = { 33 | namespaces = 34 | mapAttrs (name: ns: mkNamespace ns) config.kubernetes.namespaces; 35 | pods = 36 | mapAttrs (name: pod: mkPod pod) config.kubernetes.pods; 37 | replicationcontrollers = 38 | mapAttrs (name: ctrl: mkController ctrl) config.kubernetes.controllers; 39 | deployments = 40 | mapAttrs (name: deployment: mkDeployment deployment) config.kubernetes.deployments; 41 | daemonsets = 42 | mapAttrs (name: daemon: mkDaemonSet daemon) config.kubernetes.daemonsets; 43 | services = 44 | mapAttrs (name: service: mkService service) config.kubernetes.services; 45 | pvc = 46 | mapAttrs (name: pvc: mkPvc pvc) config.kubernetes.pvc; 47 | pv = 48 | mapAttrs (name: pv: mkPv pv) config.kubernetes.pv; 49 | storageclasses = 50 | mapAttrs (name: storageclass: mkStorageClass storageclass) config.kubernetes.storageClass; 51 | secrets = 52 | mapAttrs (name: secret: mkSecret secret) config.kubernetes.secrets; 53 | ingress = 54 | mapAttrs (name: ing: mkIngress ing) config.kubernetes.ingress; 55 | scheduledjobs = 56 | mapAttrs (name: scheduledJob: mkScheduledJob scheduledJob) config.kubernetes.scheduledJobs; 57 | roles = 58 | mapAttrs (name: role: mkRole role) config.kubernetes.roles; 59 | clusterroles = 60 | mapAttrs (name: role: mkClusterRole role) config.kubernetes.clusterRoles; 61 | rolebindings = 62 | mapAttrs (name: role: mkRoleBinding role) config.kubernetes.roleBindings; 63 | clusterrolebindings = 64 | mapAttrs (name: role: mkClusterRoleBinding role) config.kubernetes.clusterRoleBindings; 65 | serviceaccounts = 66 | mapAttrs (name: serviceAccount: mkServiceAccount serviceAccount) config.kubernetes.serviceAccounts; 67 | configmaps = 68 | mapAttrs (name: configMap: mkConfigMap configMap) config.kubernetes.configMaps; 69 | petsets = 70 | mapAttrs (name: petset: mkPetSet petset) config.kubernetes.petSets; 71 | statefulsets = 72 | mapAttrs (name: statefulset: mkStatefulSet statefulset) config.kubernetes.statefulSets; 73 | networkpolicies = 74 | mapAttrs (name: networkpolicy: mkNetworkPolicy networkpolicy) config.kubernetes.networkPolicies; 75 | poddistributionbudgets = 76 | mapAttrs (name: pdb: mkPodDistributionBudget pdb) config.kubernetes.podDistributionBudgets; 77 | customresourcedefinitions = 78 | mapAttrs (name: crd: mkCustomResourceDefinition crd) config.kubernetes.customResourceDefinitions; 79 | } // (mapAttrs (name: customResources: ( 80 | mapAttrs (name: customResource: mkCustomResource customResource) customResources 81 | )) config.kubernetes.customResources); 82 | 83 | # Keep jobs separated from other resources, as they have to be explicitly 84 | # started 85 | jobs = mapAttrs (name: job: mkJob job) config.kubernetes.jobs; 86 | 87 | gcNamespaces = config.kubernetes.gcNamespaces; 88 | }) deployments; 89 | in pkgs.stdenv.mkDerivation { 90 | name = "configurations"; 91 | buildCommand = '' 92 | cp ${pkgs.writeText "result.json" (builtins.toJSON result)} $out 93 | ''; 94 | } 95 | -------------------------------------------------------------------------------- /nix/lib.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs }: 2 | 3 | with lib; 4 | 5 | let 6 | flattenAttrs = attrs: listToAttrs ( 7 | collect (val: val ? "name" && val ? "value") ( 8 | mapAttrsRecursive (path: value: 9 | nameValuePair (concatStringsSep "." path) value 10 | ) attrs) 11 | ); 12 | 13 | 14 | mkResource = apiVersion: kind: { inherit apiVersion kind; }; 15 | 16 | mkMeta = resource: { 17 | metadata.name = resource.name; 18 | metadata.labels = resource.labels; 19 | metadata.annotations = resource.annotations // { 20 | "x-truder.net/dependencies" = concatStringsSep "," resource.dependencies; 21 | "x-truder.net/kind" = "nix-kubernetes"; 22 | "x-truder.net/version" = "v1"; 23 | }; 24 | }; 25 | 26 | mkNsMeta = resource: recursiveUpdate (mkMeta resource) { 27 | metadata.namespace = resource.namespace; 28 | }; 29 | 30 | mkSpecMeta = resource: { 31 | metadata.labels = resource.labels; 32 | metadata.annotations = resource.annotations; 33 | }; 34 | 35 | mkCommand = cmd: if isString cmd then ["sh" "-c" cmd] else cmd; 36 | 37 | filterNull = attrs: (filterAttrs (n: v: v != null) attrs); 38 | 39 | mkProbe = probe: { 40 | initialDelaySeconds = probe.initialDelaySeconds; 41 | timeoutSeconds = probe.timeoutSeconds; 42 | } // (optionalAttrs (probe.httpGet.path != null) { 43 | httpGet = probe.httpGet; 44 | }) // (optionalAttrs (probe.tcpSocket.port != null) { 45 | tcpSocket = probe.tcpSocket; 46 | }) // (optionalAttrs (probe.exec.command != null) { 47 | exec = probe.exec; 48 | }); 49 | 50 | mkContainer = container: { 51 | name = container.name; 52 | image = container.image; 53 | imagePullPolicy = container.imagePullPolicy; 54 | securityContext = container.security; 55 | ports = map (port: { 56 | inherit (port) containerPort protocol; 57 | } // (optionalAttrs (port.name != null) { 58 | inherit (port) name; 59 | }) // (optionalAttrs (port.hostPort != null) { 60 | inherit (port) hostPort; 61 | })) container.ports; 62 | volumeMounts = map (volume: ({ 63 | inherit (volume) name mountPath readOnly; 64 | } // (optionalAttrs (volume.subPath != null) { 65 | subPath = volume.subPath; 66 | }))) container.mounts; 67 | env = mapAttrsToList (name: value: { 68 | inherit name; 69 | } // (if (isAttrs value) then { 70 | valueFrom = value; 71 | } else if (isString value && hasPrefix "secret:" value) then { 72 | valueFrom.secretKeyRef = { 73 | name = head (splitString ":" (removePrefix "secret:" value)); 74 | key = head (tail (splitString ":" (removePrefix "secret:" value))); 75 | }; 76 | } else if (isString value && hasPrefix "configMap:" value) then { 77 | valueFrom.configMapKeyRef = { 78 | name = head (splitString ":" (removePrefix "configMap:" value)); 79 | key = head (tail (splitString ":" (removePrefix "configMap:" value))); 80 | }; 81 | } else { 82 | value = toString value; 83 | })) container.env; 84 | tty = container.tty; 85 | stdin = container.stdin; 86 | resources.limits = filterNull container.limits; 87 | resources.requests = filterNull container.requests; 88 | } // (optionalAttrs (container.command != null) { 89 | command = mkCommand container.command; 90 | }) // (optionalAttrs (container.args != null) { 91 | args = mkCommand container.args; 92 | }) // (optionalAttrs (container.postStart.command != null) { 93 | lifecycle.postStart.exec.command = mkCommand container.postStart.command; 94 | }) // (optionalAttrs (container.preStop.command != null) { 95 | lifecycle.preStop.exec.command = mkCommand container.preStop.command; 96 | }) // (optionalAttrs (container.livenessProbe.enable) { 97 | livenessProbe = mkProbe container.livenessProbe; 98 | }) // (optionalAttrs (container.readinessProbe.enable) { 99 | readinessProbe = mkProbe container.readinessProbe; 100 | }) // (optionalAttrs (container.workdir != null) { 101 | workingDir = container.workdir; 102 | }); 103 | 104 | mkVolume = volume: { 105 | name = volume.name; 106 | ${volume.type} = volume.options; 107 | }; 108 | 109 | mkPodSpec = resource: { 110 | spec = { 111 | nodeSelector = resource.nodeSelector; 112 | 113 | containers = mapAttrsToList (name: container: 114 | mkContainer container 115 | ) resource.containers; 116 | 117 | volumes = mapAttrsToList (name: volume: 118 | mkVolume volume 119 | ) resource.volumes; 120 | 121 | restartPolicy = resource.restartPolicy; 122 | 123 | imagePullSecrets = map (secret: { 124 | name = secret; 125 | }) resource.imagePullSecrets; 126 | 127 | hostNetwork = resource.hostNetwork; 128 | hostPID = resource.hostPID; 129 | 130 | securityContext = resource.securityContext; 131 | 132 | } // (optionalAttrs (resource.serviceAccountName != null) { 133 | serviceAccountName = resource.serviceAccountName; 134 | }) // (optionalAttrs (resource.initContainers != []) { 135 | initContainers = map (container: 136 | mkContainer container 137 | ) resource.initContainers; 138 | }); 139 | }; 140 | 141 | mkControllerSpec = rc: { 142 | spec = { 143 | replicas = rc.replicas; 144 | selector = rc.selector; 145 | template = (mkSpecMeta rc.pod) // (mkPodSpec rc.pod); 146 | }; 147 | }; 148 | 149 | mkDeploymentSpec = deployment: { 150 | spec = { 151 | replicas = deployment.replicas; 152 | template = (mkSpecMeta deployment.pod) // (mkPodSpec deployment.pod); 153 | }; 154 | }; 155 | 156 | mkDaemonSetSpec = daemon: { 157 | spec = { 158 | template = (mkSpecMeta daemon.pod) // (mkPodSpec daemon.pod); 159 | }; 160 | }; 161 | 162 | mkServiceSpec = service: { 163 | spec = { 164 | type = service.type; 165 | } // (optionalAttrs (service.clusterIP != null) { 166 | clusterIP = service.clusterIP; 167 | }) // (optionalAttrs (service.externalIPs != null) { 168 | externalIPs = service.externalIPs; 169 | }) // (optionalAttrs (service.externalName != null) { 170 | externalName = service.externalName; 171 | }) // (optionalAttrs (service.type != "ExternalName") { 172 | selector = service.selector; 173 | ports = map (port: { 174 | port = port.port; 175 | targetPort = port.targetPort; 176 | protocol = port.protocol; 177 | } // (optionalAttrs (port.name != null) { 178 | name = port.name; 179 | } // (optionalAttrs (port.nodePort != null) { 180 | nodePort = port.nodePort; 181 | }))) service.ports; 182 | }); 183 | }; 184 | 185 | mkPvcSpec = pvc: { 186 | spec = { 187 | accessModes = pvc.accessModes; 188 | resources = { 189 | requests = { 190 | storage = pvc.size; 191 | }; 192 | }; 193 | } // (optionalAttrs (pvc.selector.matchLabels != {}) { 194 | selector = { 195 | inherit (pvc.selector) matchLabels; 196 | }; 197 | }) // (optionalAttrs (pvc.storageClassName != null) { 198 | storageClassName = pvc.storageClassName; 199 | }); 200 | }; 201 | 202 | mkPvSpec = pv: { 203 | spec = { 204 | capacity.storage = pv.capacity.storage; 205 | accessModes = pv.accessModes; 206 | persistentVolumeReclaimPolicy = pv.persistentVolumeReclaimPolicy; 207 | storageClassName = pv.storageClassName; 208 | local.path = pv.local.path; 209 | }; 210 | }; 211 | 212 | mkStorageClassSpec = storageClass: { 213 | inherit (storageClass) provisioner parameters; 214 | }; 215 | 216 | mkSecretData = secret: { 217 | data = mapAttrs (name: secret: 218 | builtins.readFile (pkgs.stdenv.mkDerivation { 219 | name = "secret-${name}"; 220 | buildCommand = '' 221 | cat ${secret} | ${pkgs.coreutils}/bin/base64 -w0 > $out 222 | ''; 223 | }) 224 | ) secret.secrets; 225 | type = secret.type; 226 | }; 227 | 228 | mkIngressSpec = ing: { 229 | spec = { 230 | rules = mapAttrsToList (name: rule: { 231 | host = rule.host; 232 | http.paths = mapAttrsToList (name: path: { 233 | path = path.path; 234 | backend = path.backend; 235 | }) rule.http.paths; 236 | }) ing.rules; 237 | } // (optionalAttrs (ing.tls.secretName != null) { 238 | tls = [ 239 | ({secretName = ing.tls.secretName;} 240 | // (optionalAttrs (ing.tls.hosts != null) {hosts = ing.tls.hosts;})) 241 | ]; 242 | }); 243 | }; 244 | 245 | mkJobSpec = job: { 246 | spec.template = (mkSpecMeta job.pod) // (mkPodSpec job.pod); 247 | spec.activeDeadlineSeconds = job.activeDeadlineSeconds; 248 | }; 249 | 250 | mkScheduledJobSpec = scheduledJob: { 251 | spec = { 252 | suspend = !scheduledJob.enable; 253 | schedule = scheduledJob.schedule; 254 | jobTemplate = (mkSpecMeta scheduledJob.job) // (mkJobSpec scheduledJob.job); 255 | concurrencyPolicy = scheduledJob.concurrencyPolicy; 256 | }; 257 | }; 258 | 259 | mkNetworkPolicySpec = policy: { 260 | spec = { 261 | podSelector.matchLabels = policy.podSelector.matchLabels; 262 | ingress = mapAttrsToList (name: rule: { 263 | from = (optionals (rule.namespaceSelector.matchLabels != null) [{ 264 | namespaceSelector.matchLabels = rule.namespaceSelector; 265 | }]) ++ (optionals (rule.podSelector.matchLabels != null) [{ 266 | podSelector.matchLabels = rule.podSelector.matchLabels; 267 | }]); 268 | ports = rule.ports; 269 | }) policy.ingress; 270 | }; 271 | }; 272 | 273 | mkRoleSpec = role: { 274 | rules = map (rule: (optionalAttrs (rule.apiGroups != null) { 275 | apiGroups = rule.apiGroups; 276 | }) // (optionalAttrs (rule.resources != null) { 277 | resources = rule.resources; 278 | }) // (optionalAttrs (rule.verbs != null) { 279 | verbs = rule.verbs; 280 | }) // (optionalAttrs (rule.nonResourceURLs != null) { 281 | nonResourceURLs = rule.nonResourceURLs; 282 | })) role.rules; 283 | }; 284 | 285 | mkRoleBindingSpec = binding: { 286 | subjects = map (binding: filterAttrs (n: v: n != "_module") binding) binding.subjects; 287 | roleRef = binding.roleRef; 288 | }; 289 | 290 | mkServiceAccountSpec = serviceAccount: { 291 | }; 292 | 293 | mkConfigMapSpec = configMap: { 294 | data = flattenAttrs configMap.data; 295 | }; 296 | 297 | mkStatefulSetSpec = statefulset: { 298 | spec = { 299 | replicas = statefulset.replicas; 300 | serviceName = statefulset.serviceName; 301 | updateStrategy.type = statefulset.updateStrategy.type; 302 | podManagementPolicy = statefulset.podManagementPolicy; 303 | template = (mkSpecMeta statefulset.pod) // (mkPodSpec statefulset.pod); 304 | } // (optionalAttrs (statefulset.terminationGracePeriodSeconds != null) { 305 | terminationGracePeriodSeconds = statefulset.terminationGracePeriodSeconds; 306 | }) // (optionalAttrs ((attrNames statefulset.volumeClaimTemplates) != []) { 307 | volumeClaimTemplates = 308 | mapAttrsToList (name: claimTemplate: 309 | (mkNsMeta claimTemplate) // (mkPvcSpec claimTemplate) 310 | ) statefulset.volumeClaimTemplates; 311 | }); 312 | }; 313 | 314 | mkPetSetSpec = petset: mkStatefulSetSpec petset; 315 | 316 | mkPodDistributionBudgetSpec = pdb: { 317 | spec = { 318 | selector.matchLabels = pdb.selector.matchLabels; 319 | } // (optionalAttrs (pdb.minAvailable != null) { 320 | inherit (pdb) minAvailable; 321 | }) // (optionalAttrs (pdb.maxUnavailable != null) { 322 | inherit (pdb) maxUnavailable; 323 | }); 324 | }; 325 | 326 | mkCustomResourceExtra = customResource: 327 | mapAttrs (n: v: v) customResource.extra; 328 | 329 | mkCustomResourceDefinition = crd: { 330 | spec = { 331 | inherit (crd) group version scope; 332 | names = { 333 | inherit (crd.names) plural singular kind shortNames; 334 | }; 335 | }; 336 | }; 337 | 338 | in { 339 | mkNamespace = namespace: 340 | (mkResource "v1" "Namespace") // (mkMeta namespace); 341 | 342 | mkPod = pod: 343 | (mkResource "v1" "Pod") // (mkNsMeta pod) // 344 | (mkPodSpec pod); 345 | 346 | mkService = service: 347 | (mkResource "v1" "Service") // (mkNsMeta service) // 348 | (mkServiceSpec service); 349 | 350 | mkController = controller: 351 | (mkResource "v1" "ReplicationController") // (mkNsMeta controller) // 352 | (mkControllerSpec controller); 353 | 354 | mkDeployment = deployment: 355 | (mkResource "extensions/v1beta1" "Deployment") // (mkNsMeta deployment) // 356 | (mkDeploymentSpec deployment); 357 | 358 | mkDaemonSet = daemon: 359 | (mkResource "extensions/v1beta1" "DaemonSet") // (mkNsMeta daemon) // 360 | (mkDaemonSetSpec daemon); 361 | 362 | mkScheduledJob = scheduledJob: 363 | (mkResource "batch/v2alpha1" "ScheduledJob") // (mkNsMeta scheduledJob) // 364 | (mkScheduledJobSpec scheduledJob); 365 | 366 | mkJob = job: 367 | (mkResource "batch/v1" "Job") // (mkNsMeta job) // 368 | (mkJobSpec job); 369 | 370 | mkIngress = ingress: 371 | (mkResource "extensions/v1beta1" "Ingress") // (mkNsMeta ingress) // 372 | (mkIngressSpec ingress); 373 | 374 | mkSecret = secret: 375 | (mkResource "v1" "Secret") // (mkNsMeta secret) // 376 | (mkSecretData secret); 377 | 378 | mkPvc = pvc: 379 | (mkResource "v1" "PersistentVolumeClaim") // (mkNsMeta pvc) // 380 | (mkPvcSpec pvc); 381 | 382 | mkPv = pv: 383 | (mkResource "v1" "PersistentVolume") // (mkMeta pv) // 384 | (mkPvSpec pv); 385 | 386 | mkStorageClass = storageClass: 387 | (mkResource "storage.k8s.io/v1beta1" "StorageClass") // (mkMeta storageClass) // 388 | (mkStorageClassSpec storageClass); 389 | 390 | mkRole = role: 391 | (mkResource "rbac.authorization.k8s.io/v1beta1" "Role") // (mkNsMeta role) // 392 | (mkRoleSpec role); 393 | 394 | mkClusterRole = role: 395 | (mkResource "rbac.authorization.k8s.io/v1beta1" "ClusterRole") // (mkNsMeta role) // 396 | (mkRoleSpec role); 397 | 398 | mkRoleBinding = role: 399 | (mkResource "rbac.authorization.k8s.io/v1beta1" "RoleBinding") // (mkNsMeta role) // 400 | (mkRoleBindingSpec role); 401 | 402 | mkClusterRoleBinding = role: 403 | (mkResource "rbac.authorization.k8s.io/v1beta1" "ClusterRoleBinding") // (mkMeta role) // 404 | (mkRoleBindingSpec role); 405 | 406 | mkServiceAccount = serviceAccount: 407 | (mkResource "v1" "ServiceAccount") // (mkNsMeta serviceAccount) // 408 | (mkServiceAccountSpec serviceAccount); 409 | 410 | mkConfigMap = configMap: 411 | (mkResource "v1" "ConfigMap") // (mkNsMeta configMap) // 412 | (mkConfigMapSpec configMap); 413 | 414 | mkPetSet = petset: 415 | (mkResource "apps/v1alpha1" "PetSet") // (mkNsMeta petset) // 416 | (mkPetSetSpec petset); 417 | 418 | mkStatefulSet = statefulset: 419 | (mkResource "apps/v1beta1" "StatefulSet") // (mkNsMeta statefulset) // 420 | (mkStatefulSetSpec statefulset); 421 | 422 | mkNetworkPolicy = networkpolicy: 423 | (mkResource "extensions/v1beta1" "NetworkPolicy") // (mkNsMeta networkpolicy) // 424 | (mkNetworkPolicySpec networkpolicy); 425 | 426 | mkPodDistributionBudget = pdb: 427 | (mkResource "policy/v1beta1" "PodDisruptionBudget") // (mkNsMeta pdb) // 428 | (mkPodDistributionBudgetSpec pdb); 429 | 430 | mkCustomResource = customResource: 431 | (mkResource customResource.apiVersion customResource.kind) // 432 | (mkNsMeta customResource) // 433 | (mkCustomResourceExtra customResource); 434 | 435 | mkCustomResourceDefinition = crd: 436 | (mkResource "apiextensions.k8s.io/v1beta1" "CustomResourceDefinition") // 437 | (mkMeta crd) // 438 | (mkCustomResourceDefinition crd); 439 | } 440 | -------------------------------------------------------------------------------- /nix/options.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | 7 | cfg = config.kubernetes; 8 | 9 | probeOptions = options: { 10 | enable = mkOption { 11 | description = "Whether to enable probe"; 12 | type = types.bool; 13 | default = 14 | options.httpGet.path != null || 15 | options.exec.command != null || 16 | options.tcpSocket.port != null; 17 | }; 18 | 19 | httpGet = { 20 | path = mkOption { 21 | description = "Http check path"; 22 | type = types.nullOr types.str; 23 | default = null; 24 | }; 25 | 26 | port = mkOption { 27 | description = "Http check port"; 28 | type = types.int; 29 | default = 80; 30 | }; 31 | }; 32 | 33 | exec = { 34 | command = mkOption { 35 | description = "Command to run for check"; 36 | default = null; 37 | }; 38 | }; 39 | 40 | tcpSocket = { 41 | port = mkOption { 42 | description = "Port to connect to for check"; 43 | type = types.nullOr types.int; 44 | default = null; 45 | }; 46 | }; 47 | 48 | initialDelaySeconds = mkOption { 49 | description = "Initial delay before checking"; 50 | default = 30; 51 | type = types.int; 52 | }; 53 | 54 | timeoutSeconds = mkOption { 55 | description = "Check timeout"; 56 | default = 5; 57 | type = types.int; 58 | }; 59 | }; 60 | 61 | metaOptions = { name, config, ... } : { 62 | options = { 63 | name = mkOption { 64 | description = "Name of the resource"; 65 | type = types.str; 66 | default = name; 67 | }; 68 | 69 | labels = mkOption { 70 | description = "Resource labels"; 71 | type = types.attrsOf types.str; 72 | default = {}; 73 | }; 74 | 75 | annotations = mkOption { 76 | description = "Resource annotation"; 77 | type = types.attrsOf types.str; 78 | default = {}; 79 | }; 80 | 81 | dependencies = mkOption { 82 | description = "Resource dependencies"; 83 | type = types.listOf types.str; 84 | default = []; 85 | }; 86 | }; 87 | }; 88 | 89 | nsMetaOptions = {name, config, ...}: { 90 | options = { 91 | namespace = mkOption { 92 | description = "Resource namespace"; 93 | type = types.str; 94 | default = cfg.defaultNamespace; 95 | }; 96 | }; 97 | 98 | config = { 99 | dependencies = [("namespaces/" + config.namespace)]; 100 | }; 101 | }; 102 | 103 | containerOptions = { name, config, ... }: { 104 | options = { 105 | name = mkOption { 106 | description = "Name of the container"; 107 | default = name; 108 | type = types.str; 109 | }; 110 | 111 | enable = mkOption { 112 | description = "Whether to enable container"; 113 | default = true; 114 | type = types.bool; 115 | }; 116 | 117 | image = mkOption { 118 | description = "Image to use"; 119 | type = types.str; 120 | }; 121 | 122 | imagePullPolicy = mkOption { 123 | description = "When to pull new container image"; 124 | type = types.enum ["Always" "IfNotPresent" "Never"]; 125 | default = "Always"; 126 | }; 127 | 128 | command = mkOption { 129 | description = "Command to run"; 130 | type = types.nullOr (types.either (types.listOf types.str) types.str); 131 | default = null; 132 | }; 133 | 134 | args = mkOption { 135 | description = "Arguments to command"; 136 | type = types.nullOr (types.either (types.listOf types.str) types.str); 137 | default = null; 138 | }; 139 | 140 | env = mkOption { 141 | description = '' 142 | Environment variables to set. This can be any value that serializes to 143 | string. If value starts with "secret::" or 144 | "configMap::" if references respected secret or config map 145 | values. 146 | ''; 147 | default = {}; 148 | type = types.attrsOf types.unspecified; 149 | }; 150 | 151 | tty = mkOption { 152 | description = "Whether to enable tty"; 153 | default = false; 154 | type = types.bool; 155 | }; 156 | 157 | stdin = mkOption { 158 | description = "Whether to enable stdin"; 159 | default = false; 160 | type = types.bool; 161 | }; 162 | 163 | workdir = mkOption { 164 | description = "Container working directory"; 165 | default = null; 166 | type = types.nullOr types.path; 167 | }; 168 | 169 | postStart = { 170 | command = mkOption { 171 | description = "Command to execute in post start phase"; 172 | type = types.nullOr (types.either (types.listOf types.str) types.str); 173 | default = null; 174 | }; 175 | }; 176 | 177 | preStop = { 178 | command = mkOption { 179 | description = "Command to execute before stoping container"; 180 | type = types.nullOr (types.either (types.listOf types.str) types.str); 181 | default = null; 182 | }; 183 | }; 184 | 185 | security = { 186 | capabilities = { 187 | add = mkOption { 188 | description = "Capabilites to add"; 189 | type = types.listOf types.str; 190 | default = []; 191 | }; 192 | }; 193 | 194 | privileged = mkOption { 195 | description = "Whether to run container as privileged"; 196 | type = types.bool; 197 | default = false; 198 | }; 199 | }; 200 | 201 | ports = mkOption { 202 | description = "Ports exposed by pod"; 203 | type = types.listOf types.optionSet; 204 | default = []; 205 | 206 | options = { config, ... }: { 207 | options = { 208 | name = mkOption { 209 | description = "Name of the port"; 210 | type = types.nullOr types.str; 211 | default = null; 212 | }; 213 | 214 | protocol = mkOption { 215 | description = "Protocol to use"; 216 | default = "TCP"; 217 | type = types.enum ["TCP" "UDP"]; 218 | }; 219 | 220 | port = mkOption { 221 | description = "Port to expose"; 222 | type = types.int; 223 | }; 224 | 225 | containerPort = mkOption { 226 | description = "Port in container"; 227 | type = types.int; 228 | }; 229 | 230 | hostPort = mkOption { 231 | description = "Port on the host to bind to"; 232 | type = types.nullOr types.int; 233 | default = null; 234 | }; 235 | }; 236 | 237 | config = { 238 | containerPort = mkDefault config.port; 239 | }; 240 | }; 241 | }; 242 | 243 | mounts = mkOption { 244 | description = "Volumes mounted in pod"; 245 | type = types.listOf types.optionSet; 246 | default = []; 247 | 248 | options = { 249 | name = mkOption { 250 | description = "Name of the volume"; 251 | type = types.str; 252 | }; 253 | 254 | mountPath = mkOption { 255 | description = "Mount path of volume"; 256 | type = types.path; 257 | }; 258 | 259 | subPath = mkOption { 260 | description = "Sub path to mount"; 261 | type = types.nullOr types.str; 262 | default = null; 263 | }; 264 | 265 | readOnly = mkOption { 266 | description = "Whether to mount read only"; 267 | type = types.bool; 268 | default = false; 269 | }; 270 | }; 271 | }; 272 | 273 | requests = { 274 | memory = mkOption { 275 | description = "Limit memory for container"; 276 | type = types.nullOr types.str; 277 | example = "128Mi"; 278 | default = null; 279 | }; 280 | 281 | cpu = mkOption { 282 | description = "Limit cpu for container"; 283 | type = types.nullOr types.str; 284 | example = "500m"; 285 | default = null; 286 | }; 287 | }; 288 | 289 | limits = { 290 | memory = mkOption { 291 | description = "Limit memory for container"; 292 | type = types.nullOr types.str; 293 | example = "128Mi"; 294 | default = null; 295 | }; 296 | 297 | cpu = mkOption { 298 | description = "Limit cpu for container"; 299 | type = types.nullOr types.str; 300 | example = "500m"; 301 | default = null; 302 | }; 303 | }; 304 | 305 | livenessProbe = probeOptions config.livenessProbe; 306 | readinessProbe = probeOptions config.readinessProbe; 307 | }; 308 | }; 309 | 310 | volumeOptions = { name, config, ... }: { 311 | options = { 312 | name = mkOption { 313 | description = "Name of the volume"; 314 | type = types.str; 315 | default = name; 316 | }; 317 | 318 | type = mkOption { 319 | description = "Volume type"; 320 | type = types.str; 321 | }; 322 | 323 | options = mkOption { 324 | description = "Volume options"; 325 | type = types.attrs; 326 | default = {}; 327 | }; 328 | }; 329 | }; 330 | 331 | storageClassOptions = { name, config, ... }: { 332 | options = { 333 | provisioner = mkOption { 334 | description = "Name of the provisioner to use"; 335 | type = types.str; 336 | }; 337 | 338 | parameters = mkOption { 339 | description = "Storage provisioning parameters"; 340 | type = types.attrs; 341 | default = {}; 342 | }; 343 | }; 344 | }; 345 | 346 | podTemplateOptions = { 347 | nodeSelector = mkOption { 348 | description = "Node selector where to put pod"; 349 | type = types.attrsOf types.str; 350 | default = {}; 351 | }; 352 | 353 | containers = mkOption { 354 | description = "Pod containers"; 355 | type = types.attrsOf types.optionSet; 356 | options = [ containerOptions ]; 357 | }; 358 | 359 | initContainers = mkOption { 360 | description = "Pod init containers"; 361 | type = types.listOf types.optionSet; 362 | options = [ containerOptions ]; 363 | default = []; 364 | }; 365 | 366 | volumes = mkOption { 367 | description = "Pod volumes"; 368 | type = types.attrsOf types.optionSet; 369 | options = [ volumeOptions ]; 370 | default = {}; 371 | }; 372 | 373 | restartPolicy = mkOption { 374 | description = "Pod restart policy"; 375 | type = types.enum ["Always" "OnFailure" "Never"]; 376 | default = "Always"; 377 | }; 378 | 379 | imagePullSecrets = mkOption { 380 | description = "Name of the secret to use for pulling docker image"; 381 | type = types.listOf types.str; 382 | default = []; 383 | }; 384 | 385 | serviceAccountName = mkOption { 386 | description = "Service account name for this resource"; 387 | default = null; 388 | type = types.nullOr types.str; 389 | }; 390 | 391 | hostNetwork = mkOption { 392 | description = "Whether to use host networking"; 393 | type = types.bool; 394 | default = false; 395 | }; 396 | 397 | hostPID = mkOption { 398 | description = "Whether to use host PID"; 399 | type = types.bool; 400 | default = false; 401 | }; 402 | 403 | securityContext = { 404 | runAsUser = mkOption { 405 | description = "Run container as user id"; 406 | type = types.nullOr types.int; 407 | default = null; 408 | }; 409 | 410 | fsGroup = mkOption { 411 | description = "Container filesystem group id"; 412 | type = types.nullOr types.int; 413 | default = null; 414 | }; 415 | }; 416 | }; 417 | 418 | podOptions = { name, config, ... }: { 419 | options = podTemplateOptions; 420 | config = mkDefault cfg.defaults.pods; 421 | }; 422 | 423 | deploymentOptions = { name, config, ... }: { 424 | options = { 425 | enable = mkOption { 426 | description = "Whether to enable deployment"; 427 | default = false; 428 | type = types.bool; 429 | }; 430 | 431 | replicas = mkOption { 432 | description = "Number of replicas to run"; 433 | default = 1; 434 | type = types.int; 435 | }; 436 | 437 | pod = podTemplateOptions // { 438 | labels = mkOption { 439 | description = "Pod labels"; 440 | type = types.attrsOf types.str; 441 | default = {}; 442 | }; 443 | 444 | annotations = mkOption { 445 | description = "Pod annotation"; 446 | type = types.attrsOf types.str; 447 | default = {}; 448 | }; 449 | }; 450 | }; 451 | 452 | config = mkMerge [{ 453 | pod.labels.name = mkDefault config.name; 454 | } (mkDefault cfg.defaults.deployments)]; 455 | }; 456 | 457 | daemonSetOptions = { name, config, ... }: { 458 | options = { 459 | enable = mkOption { 460 | description = "Whether to enable daemon set"; 461 | default = false; 462 | type = types.bool; 463 | }; 464 | 465 | pod = podTemplateOptions // { 466 | labels = mkOption { 467 | description = "Pod labels"; 468 | type = types.attrsOf types.str; 469 | default = {}; 470 | }; 471 | 472 | annotations = mkOption { 473 | description = "Pod annotation"; 474 | type = types.attrsOf types.str; 475 | default = {}; 476 | }; 477 | }; 478 | }; 479 | 480 | config = mkMerge [{ 481 | pod.labels.name = mkDefault config.name; 482 | } (mkDefault cfg.defaults.daemons)]; 483 | }; 484 | 485 | replicationControllerOptions = { name, config, ... }: { 486 | options = { 487 | enable = mkOption { 488 | description = "Whether to enable controller"; 489 | default = false; 490 | type = types.bool; 491 | }; 492 | 493 | selector = mkOption { 494 | description = "Pod selector"; 495 | type = types.attrsOf types.str; 496 | default = { name = config.name; }; 497 | }; 498 | 499 | replicas = mkOption { 500 | description = "Number of replicas to run"; 501 | default = 1; 502 | type = types.int; 503 | }; 504 | 505 | pod = podTemplateOptions // { 506 | labels = mkOption { 507 | description = "Pod labels"; 508 | type = types.attrsOf types.str; 509 | default = {}; 510 | }; 511 | 512 | annotations = mkOption { 513 | description = "Pod annotation"; 514 | type = types.attrsOf types.str; 515 | default = {}; 516 | }; 517 | }; 518 | }; 519 | 520 | config = mkMerge [{ 521 | pod.labels.name = mkDefault config.name; 522 | } (mkDefault cfg.defaults.replicationcontrollers)]; 523 | }; 524 | 525 | serviceOptions = { name, config, ... }: { 526 | options = { 527 | type = mkOption { 528 | description = "Service type (ClusterIP, NodePort, LoadBalancer)"; 529 | type = types.enum ["ClusterIP" "NodePort" "LoadBalancer" "ExternalName"]; 530 | default = "ClusterIP"; 531 | }; 532 | 533 | externalName = mkOption { 534 | description = "External DNS name"; 535 | type = types.nullOr types.str; 536 | default = null; 537 | }; 538 | 539 | ports = mkOption { 540 | type = types.listOf types.optionSet; 541 | description = "Ports exposed by service"; 542 | options = { config, ... }: { 543 | options = { 544 | name = mkOption { 545 | description = "Name of the port"; 546 | type = types.nullOr types.str; 547 | default = null; 548 | }; 549 | 550 | protocol = mkOption { 551 | description = "Protocol to use"; 552 | default = "TCP"; 553 | type = types.enum ["TCP" "UDP"]; 554 | }; 555 | 556 | port = mkOption { 557 | description = "Port to expose"; 558 | type = types.int; 559 | }; 560 | 561 | targetPort = mkOption { 562 | type = types.int; 563 | description = "Pod target port"; 564 | }; 565 | 566 | nodePort = mkOption { 567 | default = null; 568 | type = types.nullOr types.int; 569 | description = "Port on the node"; 570 | }; 571 | }; 572 | 573 | config = { 574 | targetPort = mkDefault config.port; 575 | }; 576 | }; 577 | }; 578 | 579 | selector = mkOption { 580 | description = "Service selector"; 581 | type = types.attrsOf types.str; 582 | default = { inherit name; }; 583 | }; 584 | 585 | clusterIP = mkOption { 586 | default = null; 587 | type = types.nullOr types.str; 588 | description = "Cluster IP to set"; 589 | }; 590 | 591 | externalIPs = mkOption { 592 | default = null; 593 | type = types.nullOr (types.listOf types.str); 594 | description = "List of external IPs"; 595 | }; 596 | }; 597 | }; 598 | 599 | namespaceOptions = { name, config, ... }: { 600 | options = { 601 | networkPolicy.ingress.isolation = mkOption { 602 | description = "Namespace network policy"; 603 | type = types.enum [null "DefaultDeny"]; 604 | default = null; 605 | }; 606 | }; 607 | 608 | config = mkIf (config.networkPolicy.ingress.isolation != null) { 609 | annotations."net.beta.kubernetes.io/network-policy" = builtins.toJSON { 610 | ingress.isolation = config.networkPolicy.ingress.isolation; 611 | }; 612 | }; 613 | }; 614 | 615 | pvcOptions = { name, config, ... }: { 616 | options = { 617 | size = mkOption { 618 | description = "Size of storage requested by persistent volume claim"; 619 | type = types.str; 620 | default = "1G"; 621 | example = "10G"; 622 | }; 623 | 624 | accessModes = mkOption { 625 | description = "Requested acces modes"; 626 | type = types.listOf (types.enum ["ReadWriteOnce" "ReadWriteMany"]); 627 | default = ["ReadWriteOnce"]; 628 | }; 629 | 630 | storageClassName = mkOption { 631 | description = "Name of storage class requested by persistent volume claim"; 632 | type = types.nullOr types.str; 633 | default = null; 634 | }; 635 | 636 | selector.matchLabels = mkOption { 637 | description = "Persistent volume label selector"; 638 | type = types.attrs; 639 | default = {}; 640 | }; 641 | }; 642 | 643 | config = mkDefault cfg.defaults.pvc; 644 | }; 645 | 646 | pvOptions = { name, config, ... }: { 647 | options = { 648 | capacity.storage = mkOption { 649 | description = "Persistent volume storage capacity"; 650 | type = types.str; 651 | }; 652 | 653 | accessModes = mkOption { 654 | description ="Persistent volume access modes"; 655 | type = types.listOf (types.enum ["ReadWriteOnce" "ReadWriteMany"]); 656 | default = ["ReadWriteOnce"]; 657 | }; 658 | 659 | persistentVolumeReclaimPolicy = mkOption { 660 | description = "Persistent volume recalim policy"; 661 | type = types.enum ["Delete"]; 662 | default = "Delete"; 663 | }; 664 | 665 | storageClassName = mkOption { 666 | description = "Name of the storage class"; 667 | type = types.str; 668 | }; 669 | 670 | local.path = mkOption { 671 | description = "Whether this persistent volume is for local path"; 672 | type = types.nullOr types.path; 673 | default = null; 674 | }; 675 | }; 676 | }; 677 | 678 | secretOptions = { name, config, ... }: { 679 | options = { 680 | secrets = mkOption { 681 | description = "Files to include in secret"; 682 | type = types.attrs; 683 | }; 684 | 685 | type = mkOption { 686 | description = "Files to include in secret"; 687 | type = types.enum ["Opaque" "kubernetes.io/dockerconfigjson"]; 688 | default = "Opaque"; 689 | }; 690 | }; 691 | }; 692 | 693 | ingressOptions = { name, config, ... }: { 694 | options = { 695 | tls = { 696 | secretName = mkOption { 697 | description = "Name of the tls secret"; 698 | type = types.nullOr types.str; 699 | default = null; 700 | }; 701 | hosts = mkOption { 702 | description = "List of domains and sub-domains covered by certificate"; 703 | type = types.nullOr (types.listOf types.str); 704 | default = null; 705 | }; 706 | }; 707 | 708 | rules = mkOption { 709 | description = "Attribute set of rules"; 710 | type = types.attrsOf types.optionSet; 711 | options = { name, config, ... }: { 712 | options = { 713 | host = mkOption { 714 | description = "Ingress host"; 715 | type = types.nullOr types.str; 716 | }; 717 | 718 | http.paths = mkOption { 719 | description = "Attribute set of paths"; 720 | type = types.attrsOf types.optionSet; 721 | options = { name, config, ... }: { 722 | options = { 723 | path = mkOption { 724 | description = "Path to route"; 725 | type = types.str; 726 | default = name; 727 | }; 728 | 729 | backend.serviceName = mkOption { 730 | description = "Name of the service to route to"; 731 | type = types.str; 732 | }; 733 | 734 | backend.servicePort = mkOption { 735 | description = "Service port to route to"; 736 | type = types.int; 737 | default = 80; 738 | }; 739 | }; 740 | }; 741 | }; 742 | }; 743 | }; 744 | }; 745 | }; 746 | }; 747 | 748 | jobTemplateOptions = { 749 | pod = podTemplateOptions // { 750 | labels = mkOption { 751 | description = "Pod labels"; 752 | type = types.attrsOf types.str; 753 | default = {}; 754 | }; 755 | 756 | annotations = mkOption { 757 | description = "Pod annotation"; 758 | type = types.attrsOf types.str; 759 | default = {}; 760 | }; 761 | }; 762 | 763 | activeDeadlineSeconds = mkOption { 764 | description = "Job restart deadline"; 765 | default = null; 766 | type = types.nullOr types.int; 767 | }; 768 | }; 769 | 770 | jobOptions = { name, config, ... }: { 771 | options = jobTemplateOptions; 772 | config = mkMerge [{ 773 | labels.job = name; 774 | pod.restartPolicy = mkDefault "OnFailure"; 775 | } (mkDefault cfg.defaults.jobs)]; 776 | }; 777 | 778 | scheduledJobOptions = { name, config, ... }: { 779 | options = { 780 | enable = mkOption { 781 | description = "Whether to enable scheduled job"; 782 | default = true; 783 | type = types.bool; 784 | }; 785 | 786 | schedule = mkOption { 787 | description = "job schedule"; 788 | type = types.str; 789 | }; 790 | 791 | concurrencyPolicy = mkOption { 792 | description = "How to treat concurrent executions of job"; 793 | default = "Forbid"; 794 | type = types.enum ["Forbid" "Allow" "Replace"]; 795 | }; 796 | 797 | job = jobTemplateOptions // { 798 | labels = mkOption { 799 | description = "Pod labels"; 800 | type = types.attrsOf types.str; 801 | default = {}; 802 | }; 803 | 804 | annotations = mkOption { 805 | description = "Pod annotation"; 806 | type = types.attrsOf types.str; 807 | default = {}; 808 | }; 809 | }; 810 | }; 811 | 812 | config = mkMerge [{ 813 | job.labels.scheduled-job-name = name; 814 | job.pod.labels.scheduled-job-name = name; 815 | job.pod.restartPolicy = mkDefault "OnFailure"; 816 | } { 817 | job = mkDefault cfg.defaults.jobs; 818 | }]; 819 | }; 820 | 821 | networkPolicyOptions = { name, config, ... }: { 822 | options = { 823 | podSelector.matchLabels = mkOption { 824 | description = "Match pod with labels"; 825 | type = types.attrs; 826 | default = {}; 827 | }; 828 | 829 | ingress = mkOption { 830 | type = types.attrsOf types.optionSet; 831 | description = "Ingress rules"; 832 | options = [({ name, config, ... }:{ 833 | options = { 834 | namespaceSelector.matchLabels = mkOption { 835 | description = "Matches all pods in namespaces matched by this selector"; 836 | type = types.nullOr types.attrs; 837 | default = null; 838 | }; 839 | 840 | podSelector.matchLabels = mkOption { 841 | description = "Label selector which selects pods in this namespace"; 842 | type = types.nullOr types.attrs; 843 | default = { 844 | name = name; 845 | }; 846 | }; 847 | 848 | ports = mkOption { 849 | description = "List of ports which should be made accessible on the pods selected for this rule."; 850 | type = types.listOf types.attrs; 851 | default = []; 852 | options = { 853 | protocol = mkOption { 854 | description = "The protocol (TCP or UDP) which traffic must match"; 855 | type = types.str; 856 | default = "TCP"; 857 | }; 858 | 859 | port = mkOption { 860 | description = '' 861 | If specified, the port on the given protocol. If this field is not provided, 862 | this matches all port names and numbers. 863 | ''; 864 | type = types.nullOr types.int; 865 | default = null; 866 | }; 867 | }; 868 | }; 869 | }; 870 | })]; 871 | }; 872 | }; 873 | }; 874 | 875 | roleOptions = { name, config, ... }: { 876 | options = { 877 | rules = mkOption { 878 | type = types.listOf types.optionSet; 879 | description = "List of role rules"; 880 | options = [{ 881 | apiGroups = mkOption { 882 | description = "Matches list of API groups"; 883 | type = types.nullOr (types.listOf types.str); 884 | default = null; 885 | }; 886 | 887 | resources = mkOption { 888 | description = "Matches list of resources that role allows"; 889 | type = types.nullOr (types.listOf types.str); 890 | default = null; 891 | }; 892 | 893 | verbs = mkOption { 894 | description = "Matches list of verbs that role allows"; 895 | type = types.nullOr (types.listOf types.str); 896 | default = null; 897 | }; 898 | 899 | nonResourceURLs = mkOption { 900 | description = "matches the non-resource request paths (like 901 | /version and /apis)"; 902 | type = types.nullOr (types.listOf types.str); 903 | default = null; 904 | }; 905 | }]; 906 | default = []; 907 | }; 908 | }; 909 | }; 910 | 911 | roleBindingOptions = { config, ... }: { 912 | options = { 913 | subjects = mkOption { 914 | type = types.listOf types.optionSet; 915 | description = "Subjects that cluster role applies to"; 916 | options = [{ 917 | kind = mkOption { 918 | description = "To what kind of entities binding applies"; 919 | default = "User"; 920 | type = types.enum ["User" "Group" "ServiceAccount"]; 921 | }; 922 | 923 | name = mkOption { 924 | description = "Name of the entity binding applies"; 925 | type = types.str; 926 | }; 927 | 928 | namespace = mkOption { 929 | description = "Namespace of the subject"; 930 | default = null; 931 | type = types.nullOr types.str; 932 | }; 933 | }]; 934 | default = []; 935 | }; 936 | 937 | roleRef = { 938 | apiGroup = mkOption { 939 | description = "API group to use"; 940 | default = "rbac.authorization.k8s.io"; 941 | type = types.str; 942 | }; 943 | 944 | kind = mkOption { 945 | description = "Kind of the role binding references"; 946 | default = "Role"; 947 | type = types.enum ["Role" "ClusterRole"]; 948 | }; 949 | 950 | name = mkOption { 951 | description = "Name of the referenced role"; 952 | type = types.str; 953 | }; 954 | }; 955 | }; 956 | }; 957 | 958 | serviceAccountOptions = { name, config, ... }: { 959 | options = { 960 | }; 961 | }; 962 | 963 | configMapOptions = { name, config, ... }: { 964 | options = { 965 | data = mkOption { 966 | description = "Configmap data"; 967 | type = types.attrs; 968 | default = {}; 969 | }; 970 | }; 971 | }; 972 | 973 | statefulSetOptions = { name, config, ... }: { 974 | options = { 975 | enable = mkOption { 976 | description = "Whether to enable stateful set"; 977 | default = false; 978 | type = types.bool; 979 | }; 980 | 981 | terminationGracePeriodSeconds = mkOption { 982 | description = "Number of seconds to wait for termination"; 983 | default = null; 984 | type = types.nullOr types.int; 985 | }; 986 | 987 | replicas = mkOption { 988 | description = "Number of stateful set replicas to run"; 989 | default = 1; 990 | type = types.int; 991 | }; 992 | 993 | serviceName = mkOption { 994 | description = "Name of the governing stateful set service"; 995 | default = name; 996 | type = types.str; 997 | }; 998 | 999 | updateStrategy = { 1000 | type = mkOption { 1001 | description = "SatetefulSet update strategy"; 1002 | type = types.enum ["OnDelete" "RollingUpdate"]; 1003 | default = "RollingUpdate"; 1004 | }; 1005 | }; 1006 | 1007 | podManagementPolicy = mkOption { 1008 | description = "Kubernetes pod ordering stategy"; 1009 | type = types.enum ["OrderedReady" "Parallel"]; 1010 | default = "Parallel"; 1011 | }; 1012 | 1013 | pod = podTemplateOptions // { 1014 | labels = mkOption { 1015 | description = "Pod labels"; 1016 | type = types.attrsOf types.str; 1017 | default = {}; 1018 | }; 1019 | 1020 | annotations = mkOption { 1021 | description = "Pod annotation"; 1022 | type = types.attrsOf types.str; 1023 | default = {}; 1024 | }; 1025 | }; 1026 | 1027 | volumeClaimTemplates = mkOption { 1028 | type = types.attrsOf types.optionSet; 1029 | options = [ nsMetaOptions metaOptions pvcOptions ]; 1030 | default = {}; 1031 | description = "Volume claim templates"; 1032 | }; 1033 | }; 1034 | 1035 | config = mkMerge [{ 1036 | pod.labels.name = mkDefault config.name; 1037 | } (mkDefault cfg.defaults.deployments)]; 1038 | }; 1039 | 1040 | petSetOptions = statefulSetOptions; 1041 | 1042 | customResourceOptions = {name, config, ...}: { 1043 | options = { 1044 | kind = mkOption { 1045 | description = "Kind of the resource"; 1046 | type = types.str; 1047 | }; 1048 | 1049 | apiVersion = mkOption { 1050 | description = "Api version of the resource"; 1051 | type = types.str; 1052 | }; 1053 | 1054 | extra = mkOption { 1055 | type = types.attrs; 1056 | description = "Attribute set of custom resource"; 1057 | default = {}; 1058 | }; 1059 | }; 1060 | }; 1061 | 1062 | customResourceDefinitionOptions = {name, config, ...}: { 1063 | options = { 1064 | group = mkOption { 1065 | description = "Custom resource group"; 1066 | type = types.str; 1067 | }; 1068 | 1069 | version = mkOption { 1070 | description = "Custom resource version"; 1071 | type = types.str; 1072 | default = "v1"; 1073 | }; 1074 | 1075 | scope = mkOption { 1076 | description = "Custom resource scope"; 1077 | type = types.enum ["Namespaced" "Cluster"]; 1078 | default = "Namespaced"; 1079 | }; 1080 | 1081 | names = { 1082 | plural = mkOption { 1083 | description = "Plural custom resource name"; 1084 | type = types.str; 1085 | default = "${name}s"; 1086 | }; 1087 | 1088 | singular = mkOption { 1089 | description = "Singular custom resource name"; 1090 | type = types.str; 1091 | default = name; 1092 | }; 1093 | 1094 | kind = mkOption { 1095 | description = "Kind is normally the CamelCased singular type. Your resource manifests use this."; 1096 | type = types.str; 1097 | }; 1098 | 1099 | shortNames = mkOption { 1100 | description = "Custom resource definition short names"; 1101 | type = types.listOf types.str; 1102 | default = []; 1103 | }; 1104 | }; 1105 | }; 1106 | 1107 | config = { 1108 | name = mkDefault "${name}.${config.group}"; 1109 | }; 1110 | }; 1111 | 1112 | podDistributionBudgetOptions = {name, config, ...}: { 1113 | options = { 1114 | minAvailable = mkOption { 1115 | description = "Minimal number of available nodes"; 1116 | type = types.nullOr (types.either types.str types.int); 1117 | default = null; 1118 | example = "60%"; 1119 | }; 1120 | 1121 | maxUnavailable = mkOption { 1122 | description =" Maximal number of unavailable nodes"; 1123 | type = types.nullOr (types.either types.str types.int); 1124 | default = null; 1125 | example = "30%"; 1126 | }; 1127 | 1128 | selector.matchLabels = mkOption { 1129 | type = types.attrs; 1130 | description = "Labels to match pods"; 1131 | }; 1132 | }; 1133 | 1134 | config = { 1135 | selector.matchLabels = mkDefault { 1136 | inherit name; 1137 | }; 1138 | }; 1139 | }; 1140 | 1141 | in { 1142 | options.kubernetes = { 1143 | gcNamespaces = mkOption { 1144 | type = types.listOf types.str; 1145 | description = "List of namespaces to garbage collect"; 1146 | default = attrNames config.kubernetes.namespaces; 1147 | }; 1148 | 1149 | namespaces = mkOption { 1150 | type = types.attrsOf types.optionSet; 1151 | options = [ metaOptions namespaceOptions ]; 1152 | description = "Attribute set of namespaces"; 1153 | default = {}; 1154 | }; 1155 | 1156 | pods = mkOption { 1157 | type = types.attrsOf types.optionSet; 1158 | options = [ nsMetaOptions metaOptions podOptions ]; 1159 | description = "Attribute set of pods"; 1160 | default = {}; 1161 | }; 1162 | 1163 | controllers = mkOption { 1164 | type = types.attrsOf types.optionSet; 1165 | options = [ nsMetaOptions metaOptions replicationControllerOptions ]; 1166 | description = "Attribute set of controllers"; 1167 | default = {}; 1168 | }; 1169 | 1170 | deployments = mkOption { 1171 | type = types.attrsOf types.optionSet; 1172 | options = [ nsMetaOptions metaOptions deploymentOptions ]; 1173 | description = "Attribute set of deployments"; 1174 | default = {}; 1175 | }; 1176 | 1177 | daemonsets = mkOption { 1178 | type = types.attrsOf types.optionSet; 1179 | options = [ nsMetaOptions metaOptions daemonSetOptions ]; 1180 | description = "Attribute set of daemonsets"; 1181 | default = {}; 1182 | }; 1183 | 1184 | services = mkOption { 1185 | type = types.attrsOf types.optionSet; 1186 | options = [ nsMetaOptions metaOptions serviceOptions ]; 1187 | description = "Attribute set of services"; 1188 | default = {}; 1189 | }; 1190 | 1191 | pvc = mkOption { 1192 | type = types.attrsOf types.optionSet; 1193 | options = [ nsMetaOptions metaOptions pvcOptions ]; 1194 | description = "Attribute set of persistent volume claims"; 1195 | default = {}; 1196 | }; 1197 | 1198 | pv = mkOption { 1199 | type = types.attrsOf types.optionSet; 1200 | options = [ metaOptions pvOptions ]; 1201 | description = "Attibute set of persistent volumes"; 1202 | default = {}; 1203 | }; 1204 | 1205 | storageClass = mkOption { 1206 | type = types.attrsOf types.optionSet; 1207 | options = [ metaOptions storageClassOptions ]; 1208 | description = "Attribute set of storage classes"; 1209 | default = {}; 1210 | }; 1211 | 1212 | secrets = mkOption { 1213 | type = types.attrsOf types.optionSet; 1214 | options = [ nsMetaOptions metaOptions secretOptions ]; 1215 | description = "Attribute set of secrets"; 1216 | default = {}; 1217 | }; 1218 | 1219 | ingress = mkOption { 1220 | type = types.attrsOf types.optionSet; 1221 | options = [ nsMetaOptions metaOptions ingressOptions ]; 1222 | description = "Attribute set of ingress"; 1223 | default = {}; 1224 | }; 1225 | 1226 | jobs = mkOption { 1227 | type = types.attrsOf types.optionSet; 1228 | options = [ nsMetaOptions metaOptions jobOptions ]; 1229 | description = "Attribute set of jobs"; 1230 | default = {}; 1231 | }; 1232 | 1233 | scheduledJobs = mkOption { 1234 | type = types.attrsOf types.optionSet; 1235 | options = [ nsMetaOptions metaOptions scheduledJobOptions ]; 1236 | description = "Attribute set of schedule job definitions"; 1237 | default = {}; 1238 | }; 1239 | 1240 | networkPolicies = mkOption { 1241 | type = types.attrsOf types.optionSet; 1242 | options = [ nsMetaOptions metaOptions networkPolicyOptions ]; 1243 | description = "Attribute set of network policy definitions"; 1244 | default = {}; 1245 | }; 1246 | 1247 | roles = mkOption { 1248 | type = types.attrsOf types.optionSet; 1249 | options = [ nsMetaOptions metaOptions roleOptions ]; 1250 | description = "Attribute set of role definitions"; 1251 | default = {}; 1252 | }; 1253 | 1254 | clusterRoles = mkOption { 1255 | type = types.attrsOf types.optionSet; 1256 | options = [ nsMetaOptions metaOptions roleOptions ]; 1257 | description = "Attribute set of cluster role definitions"; 1258 | default = {}; 1259 | }; 1260 | 1261 | roleBindings = mkOption { 1262 | type = types.attrsOf types.optionSet; 1263 | options = [ nsMetaOptions metaOptions roleBindingOptions ]; 1264 | description = "Attribute set of role binding definitions"; 1265 | default = {}; 1266 | }; 1267 | 1268 | clusterRoleBindings = mkOption { 1269 | type = types.attrsOf types.optionSet; 1270 | options = [ nsMetaOptions metaOptions roleBindingOptions ]; 1271 | description = "Attribute set of cluster role binding definitions"; 1272 | default = {}; 1273 | }; 1274 | 1275 | serviceAccounts = mkOption { 1276 | type = types.attrsOf types.optionSet; 1277 | options = [ nsMetaOptions metaOptions serviceAccountOptions ]; 1278 | description = "Attribute set of service account definitions"; 1279 | default = {}; 1280 | }; 1281 | 1282 | configMaps = mkOption { 1283 | type = types.attrsOf types.optionSet; 1284 | options = [ nsMetaOptions metaOptions configMapOptions ]; 1285 | description = "Attribute set of config map definitions"; 1286 | default = {}; 1287 | }; 1288 | 1289 | petSets = mkOption { 1290 | type = types.attrsOf types.optionSet; 1291 | options = [ nsMetaOptions metaOptions petSetOptions ]; 1292 | description = "Attribute set of petset definitions"; 1293 | default = {}; 1294 | }; 1295 | 1296 | statefulSets = mkOption { 1297 | type = types.attrsOf types.optionSet; 1298 | options = [ nsMetaOptions metaOptions statefulSetOptions ]; 1299 | description = "Attribute set of stateful set definitions"; 1300 | default = {}; 1301 | }; 1302 | 1303 | podDistributionBudgets = mkOption { 1304 | type = types.attrsOf types.optionSet; 1305 | options = [ nsMetaOptions metaOptions podDistributionBudgetOptions ]; 1306 | description = "Attribute set of pod distribution budgets"; 1307 | default = {}; 1308 | }; 1309 | 1310 | customResources = mkOption { 1311 | description = "Attribute set of custom resources"; 1312 | type = types.attrsOf (types.attrsOf (types.submodule [ customResourceOptions nsMetaOptions metaOptions ])); 1313 | default = {}; 1314 | }; 1315 | 1316 | customResourceDefinitions = mkOption { 1317 | description = "Attribute set of custom resources definitions"; 1318 | type = types.attrsOf (types.submodule [ customResourceDefinitionOptions metaOptions ]); 1319 | default = {}; 1320 | }; 1321 | 1322 | defaultNamespace = mkOption { 1323 | type = types.str; 1324 | default = 1325 | if length (attrNames cfg.namespaces) >= 1 1326 | then (getAttr (head (attrNames cfg.namespaces)) cfg.namespaces).name 1327 | else "default"; 1328 | description = "Default namespace to put resources in"; 1329 | }; 1330 | 1331 | defaults = { 1332 | allPods = mkOption { 1333 | description = "Default config applied to all pods"; 1334 | type = types.attrs; 1335 | default = {}; 1336 | }; 1337 | 1338 | pods = mkOption { 1339 | description = "Default config applied to pods"; 1340 | type = types.attrs; 1341 | default = cfg.defaults.allPods; 1342 | }; 1343 | 1344 | jobs = mkOption { 1345 | description = "Default config applied to jobs"; 1346 | type = types.attrs; 1347 | default = { 1348 | pod = cfg.defaults.allPods; 1349 | }; 1350 | }; 1351 | 1352 | replicationcontrollers = mkOption { 1353 | description = "Default config applied to replication controllers"; 1354 | type = types.attrs; 1355 | default = { 1356 | pod = cfg.defaults.allPods; 1357 | }; 1358 | }; 1359 | 1360 | deployments = mkOption { 1361 | description = "Default config applied to deployments"; 1362 | type = types.attrs; 1363 | default = { 1364 | pod = cfg.defaults.allPods; 1365 | }; 1366 | }; 1367 | 1368 | daemons = mkOption { 1369 | description = "Default config applied to daemons"; 1370 | type = types.attrs; 1371 | default = { 1372 | pod = cfg.defaults.allPods; 1373 | }; 1374 | }; 1375 | 1376 | pvc = mkOption { 1377 | description = "Default config applied to persistent volume claims"; 1378 | type = types.attrs; 1379 | default = {}; 1380 | }; 1381 | }; 1382 | }; 1383 | } 1384 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nix-kubernetes", 3 | "version": "0.25.2", 4 | "description": "Kubernetes deployment manager written in nix", 5 | "homepage": "https://github.com/x-truder/nix-kubernetes", 6 | "author": { 7 | "name": "Jaka Hudoklin", 8 | "email": "jakahudoklin@gmail.com", 9 | "url": "https://x-truder.net" 10 | }, 11 | "bin": "bin/nix-kubernetes", 12 | "main": "lib/index.js", 13 | "keywords": [ 14 | "nix", 15 | "kubernetes", 16 | "dm", 17 | "deployment", 18 | "manager", 19 | "docker" 20 | ], 21 | "devDependencies": { 22 | "eslint": "^2.1.0", 23 | "eslint-config-xo-space": "^0.10.0", 24 | "gulp": "^3.9.0", 25 | "gulp-coveralls": "^0.1.0", 26 | "gulp-eslint": "^2.0.0", 27 | "gulp-exclude-gitignore": "^1.0.0", 28 | "gulp-istanbul": "^0.10.3", 29 | "gulp-line-ending-corrector": "^1.0.1", 30 | "gulp-mocha": "^2.0.0", 31 | "gulp-plumber": "^1.0.0", 32 | "np": "^2.14.1" 33 | }, 34 | "eslintConfig": { 35 | "extends": "xo-space", 36 | "env": { 37 | "mocha": true 38 | } 39 | }, 40 | "repository": "https://github.com/xtruder/nix-kubernetes.git", 41 | "scripts": { 42 | "test": "gulp" 43 | }, 44 | "license": "MIT", 45 | "dependencies": { 46 | "bluebird": "^3.3.4", 47 | "bluebird-retry": "^0.5.3", 48 | "cli-table2": "^0.2.0", 49 | "expand-tilde": "^1.2.0", 50 | "hoek": "^3.0.4", 51 | "lodash": "^4.6.1", 52 | "tmp": "0.0.30", 53 | "yamljs": "^0.2.8", 54 | "yargs": "^4.2.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/configmap.nix: -------------------------------------------------------------------------------- 1 | { 2 | configmap = {pkgs, ...}: { 3 | kubernetes.pods.configmappod = { 4 | dependencies = ["configmaps/config"]; 5 | 6 | containers.test = { 7 | env.KEY = "configMap:config:key1.key2.key3"; 8 | image = "redis"; 9 | ports = [{port = 6379;}]; 10 | }; 11 | volumes.test.type = "emptyDir"; 12 | }; 13 | 14 | kubernetes.configMaps.config = { 15 | data.key1.key2.key3 = "value"; 16 | }; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /test/crd.nix: -------------------------------------------------------------------------------- 1 | { 2 | crd = { 3 | kubernetes.customResourceDefinitions.etcdclusters = { 4 | group = "etcd.database.coreos.com"; 5 | version = "v1beta2"; 6 | names = { 7 | plural = "etcdclusters"; 8 | kind = "EtcdCluster"; 9 | shortNames = ["etcd"]; 10 | }; 11 | }; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /test/daemonset.nix: -------------------------------------------------------------------------------- 1 | { 2 | deployment = { 3 | kubernetes.namespaces.test = {}; 4 | kubernetes.daemonsets.test = { 5 | name = "test"; 6 | annotations.environment = "production"; 7 | 8 | pod.containers.test = { 9 | image = "redis"; 10 | ports = [{port = 6379;}]; 11 | }; 12 | }; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /test/deployment.nix: -------------------------------------------------------------------------------- 1 | { 2 | deployment = { 3 | kubernetes.namespaces.test = {}; 4 | kubernetes.deployments.test = { 5 | name = "test"; 6 | annotations.environment = "production"; 7 | 8 | replicas = 3; 9 | 10 | pod.containers.test = { 11 | image = "redis"; 12 | ports = [{port = 6379;}]; 13 | }; 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /test/gc.nix: -------------------------------------------------------------------------------- 1 | { 2 | ns1 = { 3 | kubernetes.namespaces.test = {}; 4 | 5 | kubernetes.pods.test = { 6 | annotations.environment = "production"; 7 | containers.test = { 8 | image = "redis"; 9 | ports = [{port = 6379;}]; 10 | livenessProbe.tcpSocket.port = 6379; 11 | }; 12 | volumes.test.type = "emptyDir"; 13 | }; 14 | }; 15 | 16 | ns2 = { 17 | kubernetes.namespaces.test = {}; 18 | 19 | kubernetes.pods.test2 = { 20 | annotations.environment = "production"; 21 | containers.test = { 22 | image = "redis"; 23 | ports = [{port = 6379;}]; 24 | livenessProbe.tcpSocket.port = 6379; 25 | }; 26 | volumes.test.type = "emptyDir"; 27 | }; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /test/job.nix: -------------------------------------------------------------------------------- 1 | { 2 | job = {config, lib, ...}: with lib; { 3 | options = { 4 | inspector.path = mkOption { 5 | default = "/"; 6 | type = types.str; 7 | }; 8 | }; 9 | 10 | config = { 11 | kubernetes.namespaces.test = {}; 12 | 13 | kubernetes.jobs.test = { 14 | pod.containers.test = { 15 | image = "busybox"; 16 | command = "ls -la ${config.inspector.path}"; 17 | }; 18 | }; 19 | }; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /test/netorkpolicy.nix: -------------------------------------------------------------------------------- 1 | { 2 | networkpolicy = { 3 | kubernetes.namespaces.networkpolicy = { 4 | networkPolicy.ingress.isolation = "DefaultDeny"; 5 | }; 6 | 7 | kubernetes.pods.redis = { 8 | annotations.environment = "networkpolicy"; 9 | labels.name = "redis"; 10 | containers.test = { 11 | image = "redis"; 12 | ports = [{port = 6379;}]; 13 | }; 14 | }; 15 | 16 | kubernetes.services.redis.ports = [{port = 6379;}]; 17 | 18 | kubernetes.pods.client1 = { 19 | labels.name = "client1"; 20 | containers.test = { 21 | image = "busybox"; 22 | command = ["bin/sh" "-c" '' 23 | while true; do 24 | echo ping |timeout -t 5 nc redis 6379 25 | sleep 1 26 | done 27 | '']; 28 | }; 29 | }; 30 | 31 | kubernetes.pods.client2 = { 32 | labels.name = "client2"; 33 | containers.test = { 34 | image = "busybox"; 35 | command = ["bin/sh" "-c" '' 36 | while true; do 37 | echo ping |timeout -t 5 nc redis 6379 38 | sleep 1 39 | done 40 | '']; 41 | }; 42 | }; 43 | 44 | kubernetes.networkPolicies.redis = { 45 | podSelector.matchLabels.name = "redis"; 46 | ingress.client1.ports = [{ port = 6379; }]; 47 | }; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /test/pdb.nix: -------------------------------------------------------------------------------- 1 | { 2 | pdb = { 3 | kubernetes.namespaces.test = {}; 4 | 5 | kubernetes.deployments.myapp = { 6 | replicas = 3; 7 | pod.containers.nginx.image = "nginx"; 8 | }; 9 | 10 | kubernetes.podDistributionBudgets.test = { 11 | minAvailable = "60%"; 12 | selector.matchLabels = { 13 | name = "myapp"; 14 | }; 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test/pod.nix: -------------------------------------------------------------------------------- 1 | { 2 | pod = { 3 | kubernetes.namespaces.test = {}; 4 | 5 | kubernetes.pods.test = { 6 | annotations.environment = "production"; 7 | initContainers = [{ 8 | name = "init-test"; 9 | image = "busybox"; 10 | command = "echo true"; 11 | }]; 12 | containers.test = { 13 | image = "redis"; 14 | ports = [{port = 6379;}]; 15 | livenessProbe.tcpSocket.port = 6379; 16 | }; 17 | volumes.test.type = "emptyDir"; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /test/pv.nix: -------------------------------------------------------------------------------- 1 | { 2 | pv = { 3 | kubernetes.namespaces.test = {}; 4 | 5 | kubernetes.pv.test = { 6 | capacity.storage = "100M"; 7 | accessModes = ["ReadWriteOnce"]; 8 | persistentVolumeReclaimPolicy = "Delete"; 9 | storageClassName = "local"; 10 | local.path = "/data"; 11 | }; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /test/referencing.nix: -------------------------------------------------------------------------------- 1 | { 2 | deployment1 = { 3 | kubernetes.controllers.test = { 4 | pod.containers.test = { 5 | image = "redis"; 6 | }; 7 | }; 8 | 9 | kubernetes.services.test.ports = [{port = 1000;}]; 10 | }; 11 | 12 | deployment2 = {deployments, ...}: let 13 | deployment1 = deployments.deployment1.config; 14 | in { 15 | kubernetes.deployments.test = { 16 | pod.containers.test = { 17 | image = deployment1.kubernetes.controllers.test.pod.containers.test.image; 18 | }; 19 | }; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /test/roles.nix: -------------------------------------------------------------------------------- 1 | { 2 | deployment = { 3 | kubernetes.namespaces.roles = {}; 4 | kubernetes.roles.test = { 5 | rules = [{ 6 | resources = ["pods"]; 7 | verbs = ["get" "watch" "list"]; 8 | }]; 9 | }; 10 | kubernetes.clusterRoles.test = { 11 | rules = [{ 12 | resources = ["pods"]; 13 | verbs = ["get" "watch" "list"]; 14 | }]; 15 | }; 16 | kubernetes.roleBindings.test = { 17 | subjects = [{ 18 | kind = "User"; 19 | name = "admin"; 20 | }]; 21 | roleRef.name = "test"; 22 | }; 23 | kubernetes.clusterRoleBindings.test = { 24 | subjects = [{ 25 | kind = "User"; 26 | name = "admin"; 27 | }]; 28 | roleRef.name = "test"; 29 | }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /test/scheduledJob.nix: -------------------------------------------------------------------------------- 1 | { 2 | scheduledJobs = { 3 | kubernetes.scheduledJobs.everyminute = { 4 | schedule = "0/1 * * * ?"; 5 | concurrencyPolicy = "Forbid"; 6 | job.pod.restartPolicy = "OnFailure"; 7 | job.pod.containers.hello = { 8 | image = "busybox"; 9 | args = "date; echo Hello from the Kubernetes cluster"; 10 | }; 11 | }; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /test/secret.nix: -------------------------------------------------------------------------------- 1 | { 2 | secret = {pkgs, ...}: { 3 | kubernetes.pods.secretpod = { 4 | dependencies = ["secrets/secret"]; 5 | 6 | containers.test = { 7 | env.KEY = "secret:name:key"; 8 | image = "redis"; 9 | ports = [{port = 6379;}]; 10 | }; 11 | volumes.test.type = "emptyDir"; 12 | }; 13 | 14 | kubernetes.secrets.secret = { 15 | secrets.key = "value"; 16 | }; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /test/service.nix: -------------------------------------------------------------------------------- 1 | { 2 | service = { 3 | kubernetes.namespaces.test = {}; 4 | 5 | kubernetes.services = { 6 | simple.ports = [{port = 6379;}]; 7 | multiple.ports = [ 8 | {port = 80; name = "http";} 9 | {port = 443; name = "https";} 10 | ]; 11 | withSelector = { 12 | selector.app = "mysql"; 13 | ports = [{port = 3306;}]; 14 | }; 15 | annotations = { 16 | annotations.key = "value"; 17 | ports = [{port = 3306;}]; 18 | }; 19 | withType = { 20 | type = "LoadBalancer"; 21 | ports = [{port = 3306;}]; 22 | }; 23 | with-external-name = { 24 | type = "ExternalName"; 25 | externalName = "kubernetes.default.svc.cluster.local"; 26 | ports = [{port = 80;}]; 27 | }; 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /test/serviceAccount.nix: -------------------------------------------------------------------------------- 1 | { 2 | serviceaccount = { 3 | kubernetes.namespaces.serviceaccount = {}; 4 | kubernetes.serviceAccounts.test = {}; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /test/statefulset.nix: -------------------------------------------------------------------------------- 1 | { 2 | statefulsets = { 3 | kubernetes.namespaces.test = {}; 4 | kubernetes.statefulSets.test = { 5 | name = "test"; 6 | annotations.environment = "production"; 7 | 8 | serviceName = "abcd"; 9 | replicas = 3; 10 | 11 | pod.containers.test = { 12 | image = "redis"; 13 | ports = [{port = 6379;}]; 14 | }; 15 | 16 | volumeClaimTemplates.test = { 17 | accessModes = ["ReadWriteOnce"]; 18 | size = "1G"; 19 | }; 20 | }; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /test/storageclass.nix: -------------------------------------------------------------------------------- 1 | { 2 | storageclass = { 3 | kubernetes.namespaces.test = {}; 4 | 5 | kubernetes.storageClass.test = { 6 | provisioner = "kubernetes.io/gce-pd"; 7 | parameters = { 8 | type = "pd-standard"; 9 | zone = "us-central1-a"; 10 | }; 11 | }; 12 | }; 13 | } 14 | --------------------------------------------------------------------------------