From 4796ee808153f0a6cd15cbc24845a6ed7903aa91 Mon Sep 17 00:00:00 2001 From: raffis Date: Fri, 8 Feb 2019 17:43:44 +0100 Subject: [PATCH 01/15] dev improvements --- CHANGELOG.md | 11 +++ README.md | 11 +-- config.json | 7 +- docker-compose-dev.yml | 63 +++++++++++++++++ package-lock.json | 156 +++++++++++++++++++++++++++-------------- package.json | 2 +- src/config.ts | 3 +- src/icinga.ts | 24 +++++-- src/kube/ingress.ts | 12 ++-- src/kube/node.ts | 14 ++-- src/kube/service.ts | 16 +++-- src/logger.ts | 13 ++-- src/main.ts | 6 +- 13 files changed, 245 insertions(+), 93 deletions(-) create mode 100644 docker-compose-dev.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fcfe92..7ea41cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.1.0 + +* [FIX] Fixes "(node:25116) UnhandledPromiseRejectionWarning: TypeError: result is not iterable" due error response from icinga +* [FIX] Better error handling and catching various uncaught promises +* [FIX] Docs fix regaring how to create icinga2 api user +* [CHANGE] Upgrade to winston ^3.0.0 stable +* [CHANGE] Log format is now in json and includes a timestamp +* [FIX] Service protocol is now lower cases which fixes issues like check_command=TCP insteadof check_command=tcp + + ## 1.0.1 **Maintainer**: Raffael Sahli \ **Date**: Fri 01 Jun 2018 01:18:20 PM CEST @@ -6,6 +16,7 @@ * [CHANGE] config logger.level => log.level * [FIX] catch error objects from kube watchers + ## 1.0.0 **Maintainer**: Raffael Sahli \ **Date**: Thu May 24 14:52:11 CEST 2018 diff --git a/README.md b/README.md index 85a178c..5bd8e34 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,14 @@ You may wonder where your pods are in this setup. Well it does not make any sens * Icinga2 server with enabled API module ## Setup icinga2 api user -kube-icina requires an [icinga api user](https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/#creating-apiusers) which first must be created. -You can either create it manually or using the icinga2 command utility. +kube-icina requires an [icinga api user](https://icinga.com/docs/icinga2/latest/doc/12-icinga2-api/#authentication) which first must be created. - ```sh -icinga2 api user --user kube-icinga --password kube-icinga +/etc/icinga2/conf.d/api-users.conf: +``` +object ApiUser "kube-icinga" { + password = "kube-icinga" + permissions = ["*"] +} ``` Use another password! diff --git a/config.json b/config.json index 79ffc6c..834243d 100644 --- a/config.json +++ b/config.json @@ -4,10 +4,11 @@ }, "cleanup": true, "icinga": { - "address": "", + "address": "127.0.0.1", + "format": "${info.timestamp} ${info.level}: ${info.message}", "port": 5665, - "apiUser": "", - "apiPassword": "" + "apiUser": "kube-icinga", + "apiPassword": "kube-icinga" }, "kubernetes": { "nodes": { diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..b0b30e1 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,63 @@ +version: '2' +services: + icinga2: + image: jordan/icinga2 + restart: on-failure:5 + # Set your hostname to the FQDN under which your + # sattelites will reach this container + hostname: icinga2 + env_file: + - secrets_sql.env + environment: + - ICINGA2_FEATURE_GRAPHITE=1 + # Important: + # keep the hostname graphite the same as + # the name of the graphite docker-container + - ICINGA2_FEATURE_GRAPHITE_HOST=graphite + - ICINGA2_FEATURE_GRAPHITE_PORT=2003 + - ICINGA2_FEATURE_GRAPHITE_URL=http://graphite + #- ICINGAWEB2_ADMIN_USER=icingaadmin + #- ICINGAWEB2_ADMIN_PASS=icinga + #- ICINGA2_USER_FULLNAME=Icinga2 Docker Monitoring Instance + - DEFAULT_MYSQL_HOST=mysql + volumes: + - ./data/icinga/cache:/var/cache/icinga2 + - ./data/icinga/certs:/etc/apache2/ssl + - ./data/icinga/etc/icinga2:/etc/icinga2 + - ./data/icinga/etc/icingaweb2:/etc/icingaweb2 + - ./data/icinga/lib/icinga:/var/lib/icinga2 + - ./data/icinga/lib/php/sessions:/var/lib/php/sessions + - ./data/icinga/log/apache2:/var/log/apache2 + - ./data/icinga/log/icinga2:/var/log/icinga2 + - ./data/icinga/log/icingaweb2:/var/log/icingaweb2 + - ./data/icinga/log/mysql:/var/log/mysql + - ./data/icinga/spool:/var/spool/icinga2 + # If you want to enable outbound e-mail, create the files + # - ./ssmtp/ssmtp.conf + # - ./ssmtp/revaliases + # and configure to your corresponding mail setup. + # See: https://github.com/jjethwa/icinga2#sending-notification-mails + #- ./ssmtp/revaliases:/etc/ssmtp/revaliases:ro + #- ./ssmtp/ssmtp.conf:/etc/ssmtp/ssmtp.conf:ro + ports: + - "80:80" + - "443:443" + - "5665:5665" + graphite: + image: graphiteapp/graphite-statsd:latest + container_name: graphite + restart: on-failure:5 + hostname: graphite + volumes: + - ./data/graphite/conf:/opt/graphite/conf + - ./data/graphite/storage:/opt/graphite/storage + - ./data/graphite/log/graphite:/var/log/graphite + - ./data/graphite/log/carbon:/var/log/carbon + mysql: + image: mariadb:10.1 + env_file: + - secrets_sql.env + volumes: + - ./data/mysql/data:/var/lib/mysql + # If you have previously used the container's internal DB use: + #- ./data/icinga/lib/mysql:/var/lib/mysql diff --git a/package-lock.json b/package-lock.json index d320678..1c9f75d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1067,18 +1067,21 @@ } }, "color": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/color/-/color-0.8.0.tgz", - "integrity": "sha1-iQwHw/1OZJU3Y4kRz2keVFi2/KU=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", "requires": { - "color-convert": "0.5.3", - "color-string": "0.3.0" + "color-convert": "1.9.3", + "color-string": "1.5.3" } }, "color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } }, "color-name": { "version": "1.1.3", @@ -1086,30 +1089,31 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", "requires": { - "color-name": "1.1.3" + "color-name": "1.1.3", + "simple-swizzle": "0.2.2" } }, "colornames": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/colornames/-/colornames-0.0.2.tgz", - "integrity": "sha1-2BH9bIT1kClJmorEQ2ICk1uSvjE=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" }, "colors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" }, "colorspace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.0.1.tgz", - "integrity": "sha1-yZx5btMRKLmHalLh7l7gOkpxl0k=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.1.tgz", + "integrity": "sha512-pI3btWyiuz7Ken0BWh9Elzsmv2bM9AhA7psXib4anUXy/orfZ/E0MbQwhSOG/9L8hLlalqrU0UhOuqxW1YjmVw==", "requires": { - "color": "0.8.0", - "text-hex": "0.0.0" + "color": "3.0.0", + "text-hex": "1.0.0" } }, "combined-stream": { @@ -1428,13 +1432,13 @@ "dev": true }, "diagnostics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.0.tgz", - "integrity": "sha1-4QkJALSVI+hSe+IPCBJ1IF8q42o=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", "requires": { - "colorspace": "1.0.1", + "colorspace": "1.1.1", "enabled": "1.0.2", - "kuler": "0.0.0" + "kuler": "1.0.1" } }, "diff": { @@ -1486,13 +1490,13 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", "requires": { - "env-variable": "0.0.4" + "env-variable": "0.0.5" } }, "env-variable": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.4.tgz", - "integrity": "sha512-+jpGxSWG4vr6gVxUHOc4p+ilPnql7NzZxOZBxNldsKGjCF+97df3CbuX7XMaDa5oAVkKQj4rKp38rYdC4VcpDg==" + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", + "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" }, "error-ex": { "version": "1.3.1", @@ -1957,6 +1961,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" + }, "fb-watchman": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", @@ -4001,11 +4010,11 @@ } }, "kuler": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-0.0.0.tgz", - "integrity": "sha1-tmu0a5NOVQ9Z2BiEjgq7pPf1VTw=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", "requires": { - "colornames": "0.0.2" + "colornames": "1.1.1" } }, "lazy-cache": { @@ -4157,11 +4166,12 @@ "dev": true }, "logform": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-1.7.0.tgz", - "integrity": "sha512-IyyAkQiA0I3LEar69J0bR5kg7204883jsWW4os9ypXiEsHueuPwfEClC3aSelhG+pIbMD0l23nAkz5VRXZYMWA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", "requires": { - "colors": "1.3.0", + "colors": "1.3.3", + "fast-safe-stringify": "2.0.6", "fecha": "2.3.3", "ms": "2.1.1", "triple-beam": "1.3.0" @@ -5493,6 +5503,21 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "0.3.2" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -5938,9 +5963,9 @@ } }, "text-hex": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-0.0.0.tgz", - "integrity": "sha1-V4+8haapJjbkLdF7QdAhjM6esrM=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, "text-table": { "version": "0.2.0", @@ -6645,24 +6670,49 @@ "optional": true }, "winston": { - "version": "3.0.0-rc5", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.0.0-rc5.tgz", - "integrity": "sha512-BRYS7jsNkfLAqGu4dZW3kp6CmqiWKrComvfsIMYdsnpPre7g8BIw63nWRyX69vxyLYvZdszcEyxJkJmILXd/pA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", "requires": { - "async": "2.6.0", - "diagnostics": "1.1.0", + "async": "2.6.1", + "diagnostics": "1.1.1", "is-stream": "1.1.0", - "logform": "1.7.0", + "logform": "2.1.2", "one-time": "0.0.4", + "readable-stream": "3.1.1", "stack-trace": "0.0.10", "triple-beam": "1.3.0", - "winston-transport": "3.2.1" + "winston-transport": "4.3.0" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "4.17.10" + } + }, + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "requires": { + "inherits": "2.0.3", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + } } }, "winston-transport": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-3.2.1.tgz", - "integrity": "sha512-WPqbdAmMK/kfWCWKM2bA1o997wWPZ0jg5NpO8JPqoaDZgCiZnFpIEddcf7Ur4UZV2sYRUdySVBWKx0+gQQ/jrg==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "requires": { + "readable-stream": "2.3.6", + "triple-beam": "1.3.0" + } }, "wordwrap": { "version": "1.0.0", diff --git a/package.json b/package.json index 004d75d..d258246 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "icinga2-api": "raffis/nodejs-icinga2api", "json-stream": "^1.0.0", "kubernetes-client": "^5.3.0", - "winston": "^3.0.0-rc5" + "winston": "^3.0.0" }, "devDependencies": { "@types/jest": "^22.2.3", diff --git a/src/config.ts b/src/config.ts index bd356af..cd6f3c5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ try { const config = { log: { level: process.env.LOG_LEVEL || defaultConfig.log.level || 'info', + format: process.env.LOG_FORMAT || defaultConfig.log.format || '${info.timestamp} ${info.level}: ${info.message}' }, cleanup: process.env.CLEANUP || defaultConfig.cleanup || true, icinga: { @@ -65,5 +66,5 @@ const config = { }, }, }; - +console.log(config); export default config; diff --git a/src/icinga.ts b/src/icinga.ts index 4a3d2ba..8a58746 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -1,17 +1,17 @@ -import {LoggerInstance} from 'winston'; +import {Logger} from 'winston'; import IcingaClient from 'icinga2-api'; /** * icinga wrapper */ export default class Icinga { - protected logger: LoggerInstance; + protected logger: Logger; protected icingaClient: IcingaClient; /** * icinga wrapper */ - constructor(logger: LoggerInstance, icingaClient: IcingaClient) { + constructor(logger: Logger, icingaClient: IcingaClient) { this.logger = logger; this.icingaClient = icingaClient; } @@ -26,7 +26,7 @@ export default class Icinga { if (err.Statuscode == '404') { resolve(false); } else { - reject(); + reject(err); } } else { resolve(true); @@ -124,7 +124,7 @@ export default class Icinga { } }); } else { - reject(); + reject(err); } } }); @@ -203,7 +203,12 @@ export default class Icinga { return new Promise((resolve, reject) => { this.icingaClient.getServiceFiltered({filter: 'service.vars._kubernetes == true'}, async (err, result) => { - const handlers = []; + if(err) { + return reject(err); + } + + console.log(result, err); + var handlers = []; for (const service of result) { handlers.push(this.deleteService(service.attrs.host_name, service.attrs.name)); } @@ -213,7 +218,12 @@ export default class Icinga { }); this.icingaClient.getHostFiltered({filter: 'host.vars._kubernetes == true'}, async (err, result) => { - const handlers = []; + if(err) { + return reject(err); + } + + console.log(result); + var handlers = []; for (const host of result) { handlers.push(this.deleteHost(host.attrs.name)); } diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index a0e748d..6add3d0 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -1,4 +1,4 @@ -import {LoggerInstance} from 'winston'; +import {Logger} from 'winston'; import Icinga from '../icinga'; import JSONStream from 'json-stream'; import KubeNode from './node'; @@ -7,7 +7,7 @@ import KubeNode from './node'; * kubernetes ingresses */ export default class Ingress { - protected logger: LoggerInstance; + protected logger: Logger; protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; @@ -24,7 +24,7 @@ export default class Ingress { /** * kubernetes ingresses */ - constructor(logger: LoggerInstance, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: object={}) { + constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: object={}) { this.logger = logger; this.kubeClient = kubeClient; this.icinga = icinga; @@ -113,7 +113,7 @@ export default class Ingress { const stream = this.kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { - this.logger.debug('received kubernetes ingress', {object}); + this.logger.debug('received kubernetes ingress resource', {object}); if(object.object.kind !== 'Ingress') { this.logger.error('skip invalid ingress object', {object: object}); @@ -125,7 +125,9 @@ export default class Ingress { } if (object.type == 'ADDED' || object.type == 'MODIFIED') { - this.prepareObject(object.object); + this.prepareObject(object.object).catch(err => { + this.logger.error('failed to handle resource', {error: err}) + }); } }); diff --git a/src/kube/node.ts b/src/kube/node.ts index a5891ca..20eec5c 100644 --- a/src/kube/node.ts +++ b/src/kube/node.ts @@ -1,4 +1,4 @@ -import {LoggerInstance} from 'winston'; +import {Logger} from 'winston'; import Icinga from '../icinga'; import JSONStream from 'json-stream'; @@ -6,7 +6,7 @@ import JSONStream from 'json-stream'; * kubernetes hosts */ export default class Node { - protected logger: LoggerInstance; + protected logger: Logger; protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; @@ -20,7 +20,7 @@ export default class Node { /** * kubernetes hosts */ - constructor(logger: LoggerInstance, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + constructor(logger: Logger, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { this.logger = logger; this.kubeClient = kubeClient; this.icinga = icinga; @@ -46,7 +46,7 @@ export default class Node { } host = Object.assign(host, this.options.hostDefinition); - this.icinga.applyHost(host.host_name, host.host_name, host, this.options.hostTemplates); + return this.icinga.applyHost(host.host_name, host.host_name, host, this.options.hostTemplates); } /** @@ -69,7 +69,7 @@ export default class Node { return; } - this.logger.debug('received kubernetes host', {object}); + this.logger.debug('received kubernetes host resource', {object}); if(object.object.kind !== 'Node') { this.logger.error('skip invalid node object', {object: object}); return; @@ -80,7 +80,9 @@ export default class Node { } if (object.type == 'ADDED') { - this.prepareObject(object.object); + this.prepareObject(object.object).catch(err => { + this.logger.error('failed to handle resource', {error: err}); + }); } }); diff --git a/src/kube/service.ts b/src/kube/service.ts index 555d4f1..e49d85a 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -1,4 +1,4 @@ -import {LoggerInstance} from 'winston'; +import {Logger} from 'winston'; import Icinga from '../icinga'; import JSONStream from 'json-stream'; import KubeNode from './node'; @@ -11,7 +11,7 @@ export default class Service { static readonly TYPE_NODEPORT = 'NodePort'; static readonly TYPE_LOADBALANCER = 'LoadBalancer'; - protected logger: LoggerInstance; + protected logger: Logger; protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; @@ -46,7 +46,7 @@ export default class Service { /** * kubernetes services */ - constructor(logger: LoggerInstance, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: object={}) { + constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: object={}) { this.logger = logger; this.kubeClient = kubeClient; this.icinga = icinga; @@ -111,7 +111,7 @@ export default class Service { if (hasCommand) { this.logger.debug('service can be checked via check command '+name); service = { - 'check_command': name, + 'check_command': name.toLowerCase(), 'display_name': name, 'vars._kubernetes': true, 'vars.kubernetes': definition, @@ -127,7 +127,7 @@ export default class Service { let protocol = servicePort.protocol.toLowerCase(); let name = servicePort.name || protocol+':'+servicePort.port; service = { - 'check_command': servicePort.protocol, + 'check_command': protocol, 'display_name': name.toLowerCase(), 'vars._kubernetes': true, 'vars.kubernetes': definition, @@ -151,7 +151,7 @@ export default class Service { const stream = this.kubeClient.apis.v1.watch.services.getStream(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { - this.logger.debug('received kubernetes service', {object}); + this.logger.debug('received kubernetes service resource', {object}); if(object.object.kind !== 'Service') { this.logger.error('skip invalid service object', {object: object}); @@ -168,7 +168,9 @@ export default class Service { } if (object.type == 'ADDED' || object.type == 'MODIFIED') { - this.prepareObject(object.object); + this.prepareObject(object.object).catch(err => { + this.logger.error('failed to handle resource', {error: err}) + }); } }); diff --git a/src/logger.ts b/src/logger.ts index 4728c8a..60477aa 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,11 +1,16 @@ -import * as winston from 'winston'; +import {createLogger, format, transports} from 'winston'; import config from './config'; -const logger = winston.createLogger({ +const logger = createLogger({ level: config.log.level, - format: winston.format.simple(), + format: format.combine( + format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + format.json() + ), transports: [ - new winston.transports.Console(), + new transports.Console(), ], }); diff --git a/src/main.ts b/src/main.ts index c11944f..9d23bc3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,11 +18,13 @@ const kubeService = new Service(logger, kubeNode, kubeClient, icinga, new JSONSt */ async function main() { if (config.cleanup) { - await icinga.cleanup(); + await icinga.cleanup().catch(err => { + logger.error('failed to cleanup icinga objects', {error: err}); + }); } if (config.kubernetes.nodes.discover) { - kubeNode.kubeListener(); + kubeNode.kubeListener(); } if (config.kubernetes.ingresses.discover) { From ae9e39c02902ef88f0a2fa1a23546e71ff8769c6 Mon Sep 17 00:00:00 2001 From: raffis Date: Tue, 12 Feb 2019 16:33:21 +0100 Subject: [PATCH 02/15] fixes #4 --- src/kube/volume.ts | 136 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/kube/volume.ts diff --git a/src/kube/volume.ts b/src/kube/volume.ts new file mode 100644 index 0000000..e6c86b5 --- /dev/null +++ b/src/kube/volume.ts @@ -0,0 +1,136 @@ +import {Logger} from 'winston'; +import Icinga from '../icinga'; +import JSONStream from 'json-stream'; +import KubeNode from './node'; +import Resource from './abstract.resource'; + +/** + * kubernetes ingresses + */ +export default class Volume extends Resource { + protected logger: Logger; + protected kubeClient; + protected icinga: Icinga; + protected jsonStream: JSONStream; + protected kubeNode: KubeNode; + protected options = { + applyServices: true, + attachToNodes: false, + hostDefinition: {}, + serviceDefinition: {}, + hostTemplates: [], + serviceTemplates: [], + }; + + /** + * kubernetes hosts + */ + constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + super(); + this.logger = logger; + this.kubeClient = kubeClient; + this.icinga = icinga; + this.jsonStream = jsonStream; + this.kubeNode = kubeNode; + this.options = Object.assign(this.options, options); + } + + /** + * Apply host + */ + protected async applyHost(name: string, address: string, metadata, templates: string[]) { + let definition = { + 'display_name': name, + 'address': address, + 'check_command': 'dummy', + 'vars.dummy_state': 0, + 'vars._kubernetes': true, + 'vars.kubernetes': metadata, + }; + + Object.assign(definition, this.options.hostDefinition); + return this.icinga.applyHost(name, address, definition, this.options.hostTemplates); + } + + /** + * Apply service + */ + protected async applyService(host: string, name: string, definition, templates: string[]) { + if (this.options.attachToNodes) { + for (const node of this.kubeNode.getWorkerNodes()) { + definition.host_name = node; + this.icinga.applyService(node, name, definition, templates); + } + } else { + definition.host_name = host; + this.icinga.applyService(host, name, definition, templates); + } + } + + /** + * Preapre icinga object and apply + */ + public async prepareObject(definition: any): Promise { + var host = this.escapeName(definition.metadata.annotations['pv.kubernetes.io/provisioned-by']); + await this.applyHost(host, host, definition, this.options.hostTemplates); + + if (this.options.applyServices) { + var groups = []; + if(definition.spec.claimRef.namespace) { + groups.push(definition.spec.claimRef.namespace) + await this.icinga.applyServiceGroup(definition.spec.claimRef.namespace); + } + + let templates = this.options.serviceTemplates; + templates = templates.concat(this.prepareTemplates(definition)); + + let service = this.options.serviceDefinition; + let name = this.escapeName(definition.metadata.name); + let addition = { + 'check_command': 'dummy', + 'display_name': `${definition.metadata.name}:volume`, + 'vars._kubernetes': true, + 'vars.kubernetes': definition, + 'groups': groups, + }; + + Object.assign(addition, service); + Object.assign(addition, this.prepareResource(definition)); + this.applyService(host, name, addition, templates); + } + } + + /** + * Start kube listener + */ + public async kubeListener(): Promise { + try { + const stream = this.kubeClient.apis.v1.watch.persistentvolumes.getStream(); + stream.pipe(this.jsonStream); + this.jsonStream.on('data', async (object) => { + this.logger.debug('received kubernetes persistent volume resource', {object}); + + if(object.object.kind !== 'PersistentVolume') { + this.logger.error('skip invalid object', {object: object}); + return; + } + + if (object.type == 'MODIFIED' || object.type == 'DELETED') { + await this.icinga.deleteHost(object.object.metadata.name); + } + + if (object.type == 'ADDED' || object.type == 'MODIFIED') { + this.prepareObject(object.object).catch(err => { + this.logger.error('failed to handle resource', {error: err}) + }); + } + }); + + this.jsonStream.on('finish', () => { + this.kubeListener(); + }); + } catch (err) { + this.logger.error('failed start ingresses listener', {error: err}); + } + } +} From 45245053649a340b0f0ed4704cfb4209802c7e7a Mon Sep 17 00:00:00 2001 From: raffis Date: Tue, 12 Feb 2019 16:33:42 +0100 Subject: [PATCH 03/15] fixes #1 --- src/kube/service.ts | 121 ++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/src/kube/service.ts b/src/kube/service.ts index e49d85a..e3ed7f2 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -2,23 +2,26 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; import JSONStream from 'json-stream'; import KubeNode from './node'; +import Resource from './abstract.resource'; + +interface ServiceTypeOptions { + discover?: boolean; + applyServices?: boolean; + hostDefinition?: any; + serviceDefinition?: any; + hostTemplates?: string[]; + serviceTemplates?: string[]; +} -/** - * kubernetes services - */ -export default class Service { - static readonly TYPE_CLUSTERIP = 'ClusterIP'; - static readonly TYPE_NODEPORT = 'NodePort'; - static readonly TYPE_LOADBALANCER = 'LoadBalancer'; +interface ServiceOptions { + ClusterIP?: ServiceTypeOptions; + NodePort?: ServiceTypeOptions; + LoadBalancer?: ServiceTypeOptions; +} - protected logger: Logger; - protected kubeClient; - protected icinga: Icinga; - protected jsonStream: JSONStream; - protected kubeNode: KubeNode; - protected options = { +const defaults: ServiceOptions = { ClusterIP: { - discovery: false, + discover: false, applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -26,7 +29,7 @@ export default class Service { serviceTemplates: [], }, NodePort: { - discovery: true, + discover: true, applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -34,24 +37,44 @@ export default class Service { serviceTemplates: [], }, LoadBalancer: { - discovery: true, + discover: true, applyServices: true, hostDefinition: {}, serviceDefinition: {}, hostTemplates: [], serviceTemplates: [], }, - }; +}; + +/** + * kubernetes services + */ +export default class Service extends Resource { + static readonly TYPE_CLUSTERIP = 'ClusterIP'; + static readonly TYPE_NODEPORT = 'NodePort'; + static readonly TYPE_LOADBALANCER = 'LoadBalancer'; + + protected logger: Logger; + protected kubeClient; + protected icinga: Icinga; + protected jsonStream: JSONStream; + protected kubeNode: KubeNode; + protected options: ServiceOptions = defaults; /** * kubernetes services */ - constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: object={}) { + constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: ServiceOptions=defaults) { + super(); this.logger = logger; this.kubeClient = kubeClient; this.icinga = icinga; this.jsonStream = jsonStream; - this.options = Object.assign(this.options, options); + var clone = JSON.parse(JSON.stringify(defaults)); + Object.assign(clone.ClusterIP, options.ClusterIP); + Object.assign(clone.NodePort, options.NodePort); + Object.assign(clone.LoadBalancer, options.LoadBalancer); + this.options = clone; this.kubeNode = kubeNode; } @@ -76,7 +99,7 @@ export default class Service { * Apply service */ protected async applyService(host: string, name: string, type: string, definition, templates: string[]) { - if (type === Service.TYPE_NODEPORT) { + if (type === Service.TYPE_NODEPORT) { for (const node of this.kubeNode.getWorkerNodes()) { definition.host_name = node; this.icinga.applyService(node, name, definition, templates); @@ -92,8 +115,18 @@ export default class Service { */ public async prepareObject(definition): Promise { let serviceType = definition.spec.type; - let options = this.options[serviceType]; + if(!this.options[serviceType]) { + throw new Error('unknown service type provided'); + } + + var options = this.options[serviceType]; + var service = options.serviceDefinition; + service['groups'] = [definition.metadata.namespace]; + Object.assign(service, this.prepareResource(definition)); + + var templates = options.serviceTemplates; + templates = templates.concat(this.prepareTemplates(definition)); if (serviceType !== Service.TYPE_NODEPORT) { await this.applyHost(definition.metadata.name, definition.spec.clusterIP, serviceType, definition, options.hostTemplates); } @@ -102,43 +135,31 @@ export default class Service { await this.icinga.applyServiceGroup(definition.metadata.namespace); for (const servicePort of definition.spec.ports) { - let service; - - if (servicePort.name && options.portNameAsCommand) { - let name = servicePort.name.toLowerCase(); - let hasCommand = await this.icinga.hasCheckCommand(name); - + let port = JSON.parse(JSON.stringify(service)); + if (port.check_command) { + let hasCommand = await this.icinga.hasCheckCommand(port.check_command); if (hasCommand) { - this.logger.debug('service can be checked via check command '+name); - service = { - 'check_command': name.toLowerCase(), - 'display_name': name, - 'vars._kubernetes': true, - 'vars.kubernetes': definition, - 'groups': [definition.metadata.namespace], - }; - service['vars.'+servicePort.name+'_port'] = servicePort.nodePort || servicePort.port; + this.logger.debug('service can be checked via check command '+port.check_command); + port.display_name = name; + port['vars.'+port.check_command+'_port'] = servicePort.nodePort || servicePort.port; } else { - this.logger.warn('service can not be checked via check command '+servicePort.name+', icinga check command does not exists, fallback to '+servicePort.protocol); + delete port.check_command; + this.logger.warn('service can not be checked via check command '+port.check_command+', icinga check command does not exists, fallback to service protocol '+servicePort.protocol); } } - if (!service) { + if (!port.check_command) { let protocol = servicePort.protocol.toLowerCase(); let name = servicePort.name || protocol+':'+servicePort.port; - service = { - 'check_command': protocol, - 'display_name': name.toLowerCase(), - 'vars._kubernetes': true, - 'vars.kubernetes': definition, - 'groups': [definition.metadata.namespace], - }; - - service['vars.'+protocol+'_port'] = servicePort.nodePort || servicePort.port; + port.check_command = protocol; + port.display_name = name.toLowerCase(); + port['vars.'+protocol+'_port'] = servicePort.nodePort || servicePort.port; } - Object.assign(service, options.serviceDefinition); - this.applyService(definition.metadata.name, service.display_name, serviceType, service, options.serviceTemplates); + port['vars._kubernetes'] = true; + port['vars.kubernetes'] = definition; + + this.applyService(definition.metadata.name, port.display_name, serviceType, port, templates); } } } @@ -159,7 +180,7 @@ export default class Service { } if (!this.options[object.object.spec.type].discover) { - this.logger.debug('skip service object, since ['+object.object.spec.type+'] is not enabled for discovery', {object: object}); + this.logger.debug('skip service object, since ['+object.object.spec.type+'] is not enabled for discover', {object: object}); return; } From de343327c87f8311028858726cbb33228cedb711 Mon Sep 17 00:00:00 2001 From: raffis Date: Tue, 12 Feb 2019 16:34:07 +0100 Subject: [PATCH 04/15] added new tests --- CHANGELOG.md | 5 + README.md | 120 +- config.json | 24 +- package-lock.json | 2813 +++++++++++++++++++++------------ package.json | 9 +- src/config.ts | 18 +- src/icinga.ts | 4 +- src/kube/abstract.resource.ts | 55 + src/kube/ingress.ts | 35 +- src/kube/node.ts | 4 +- src/main.ts | 12 +- tests/kube/ingress.test.ts | 114 +- tests/kube/service.test.ts | 294 ++++ 13 files changed, 2339 insertions(+), 1168 deletions(-) create mode 100644 src/kube/abstract.resource.ts create mode 100644 tests/kube/service.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ea41cc..a92b12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ * [CHANGE] Upgrade to winston ^3.0.0 stable * [CHANGE] Log format is now in json and includes a timestamp * [FIX] Service protocol is now lower cases which fixes issues like check_command=TCP insteadof check_command=tcp +* [FIX] Fixes icinga api username is now correctly read from ICINGA_API_USERNAME as documented (instead ICINGA_API_USER) +* [FEATURE] Add support for service/host definitions in kubernetes annotations #1 +* [BREAKER!] Removed service.portNameAsCommand option, use kubernetes annotations +* [FEATURE] Discover persistent volumes and create icinga services for those #4 +* [FIX] Fixed typo Definiton => Definition in default config example ## 1.0.1 diff --git a/README.md b/README.md index 5bd8e34..4ee98cb 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,22 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![GitHub release](https://img.shields.io/github/release/gyselroth/kube-icinga.svg)](https://github.com/gyselroth/kube-icinga/releases) -kube-icinga will automatically deploy icinga objects to monitor your kubernetes services. +kube-icinga automatically deploys icinga objects out of your kubernetes resources. It has built-in autodiscovery and will work out of the box. However you can change various default configurations and deploy custom icinga objects or disable/enable kubernetes objects to monitor. ## How does it work? -Multiple watchers are bootstraped and listen for any kubernetes changes. Those changes will reflect immediately on icinga2. +Multiple watchers are bootstraped and listen for any kubernetes changes. Those changes will reflect immediately on icinga. * Kubernetes namespaces will result in icinga service groups and host groups -* Nodes will result in host objects -* Ingresses will result in dummy icinga host objects (hostname is taken from metadata.name) whereas ingress paths will get deployed as icinga services related to the ingress host object ->**Note**: You may change this behaviour by attaching the services (ingress paths) to all kubernetes worker nodes by setting `kubernetes.ingresses.attachToNodes` to `true`. (This will result in many more services and checks depending on the size of your cluster!) -* Services (ClusterIP, NodePort, LoadBalanacer) will also result in icinga host objects and service ports are icinga services. ->**Note**: NodePort services will always be attached to each kubernetes worker node. See [NodePort](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) services for more information. +* Nodes will result in host objects +* Ingresses will result in dummy icinga host objects [1] (hostname is taken from metadata.name) whereas ingress paths will get deployed as icinga services related to the ingress host object +* Services (ClusterIP, NodePort [2], LoadBalanacer) will also result in icinga host objects and service ports are icinga services. +* Persistent volumes will result in services attached to a dummy host + +>**Note**: [1] You may change this behaviour by attaching the services (ingress paths) to all kubernetes worker nodes by setting `kubernetes.ingresses.attachToNodes` to `true`. (This will result in many more services and checks depending on the size of your cluster!) +>**Note**: [2] NodePort services will always be attached to each kubernetes worker node. See [NodePort](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) services for more information. Since there is no such thing as hosts in the world of moving and containers. The host object (kubernetes metadata.name) on icinga may just be a [dummy](https://www.icinga.com/docs/icinga2/latest/doc/10-icinga-template-library/#plugin-check-command-dummy) and will not get checked if the service can not be attached to kubernetes nodes. @@ -30,14 +32,14 @@ You may wonder where your pods are in this setup. Well it does not make any sens * [Requirements](#requirements) * [Setup icinga2 api user](#setup-icinga2-api-user) * [Deployment](#deployment) -* [Configuration](#configuration) * [Advanced topics](#advanced-topics) * [ClusterIP services](#clusterip-services) * [Using icinga2 apply rules](#using-icinga2-apply-rules) * [Overwite specific icinga object definition](#overwite-specific-icinga-object-definition) +* [Configuration](#configuration) ## Requirements -* A running kubernetes cluster (or minikube) +* A running kubernetes cluster * Icinga2 server with enabled API module ## Setup icinga2 api user @@ -50,8 +52,6 @@ object ApiUser "kube-icinga" { permissions = ["*"] } ``` -Use another password! - ## Deployment The recommended (and logical) way to deploy kube-icinga is deplyoing kube-icinga on kubernetes itself using the [gyselroth/kube-icinga](https://hub.docker.com/r/gyselroth/kube-icinga/) docker image. @@ -62,6 +62,69 @@ kubectl -f https://raw.githubusercontent.com/gyselroth/kube-icinga/master/kube-i ``` (Change the secret password and ICINGA_ADDRESS value accordingly) + +## Advanced topics + +### ClusterIP services +Ingresses, NodePort and Load Balancer services will expose your kubernetes services to the public but not ClusterIP services. ClusterIP services (Which is the default) are only internal kubernetes services and not available from outside the cluster. If your icinga2 setup lives outside the cluster you only have to options, either disable deployment for those objects or setup an icinga [satelite](https://www.icinga.com/docs/icinga2/latest/doc/06-distributed-monitoring) container which will live in kubernetes. Of course you may also deploy an entire icinga2 setup on kubernetes if you do not have an existing one. + +### Using icinga2 apply rules +You certainly can use icinga2 apply rules. You may disable auto service deployments via `applyServices` for ingresses and services and define your own services via [apply rules](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#using-apply). +Of course you can also setup a mixed deployment. Automatically let kube-icinga deploy services and apply additional services via apply rules. + +>**Note**: Since icinga apply rules are [not triggered](https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/#modifying-objects) if an object gets updated kube-icinga will delete and recreate those objects. + +All icinga host object are created with all kubernetes data available packed in the variable `vars.kubernetes`. Therefore you can apply rules with this data, for example this will create an icinga service for all objects with a kubernetes label foo=bar. + +### Globally overwrite icinga object definitions +It is possible to set custom options for the icinga objects during creation. You might set custom values via `kubernetes.ingresses.hostTemplates` or kubernetes.ingresses.serviceTemplate`. The same can also be done for services. For example it might be crucial to set a custom zone for ClusterIP services since they are only reachable within the cluster and shall be monitored by an icinga satelite node. Any valid setting for a specific icinga object can be set. + +>**Note** This will set the custom options for all objects from the same type (services.ClusterIP in the follwing example), this may not what you want. Using kubernetes annotations +you may configure custom incinga attributes directly in the kubernetes resource definition. + +```json +{ + "kubernetes": { + "services": { + "ClusterIP": + "discover": true, + "serviceDefiniton": { + "zone": "my_custom_icinga_zone" + "vars.a_custom_icinga_variable": ["foo", "bar"] + }, + } + } + } +} +``` + +### Overwrite icinga object definitions directly in kubernetes resources + +kube-icinga is able to parse kubernetes annotations and merge those with its default settings. +You may use the following annotations: + +| Name | Description | +| `kube-icinga/check_command` | Use a custom icinga check command. | +| `kube-icinga/template` | Use a custom icinga template. | +| `kube-icinga/definition` | JSON encoded icinga definiton which may contain advanced icinga options and gets merged with the defaults. | + + +```yaml +kind: PersistentVolume +apiVersion: v1 +metadata: + name: generic-nimble-24afe01f-2300-11e9-94e3-0050568fe3c2, + selfLink":"/api/v1/persistentvolumes/generic-nimble-24afe01f-2300-11e9-94e3-0050568fe3c2 + uid":"2a66425e-2300-11e9-94e3-0050568fe3c2 + resourceVersion: 65603642 + creationTimestamp: 2019-01-28T13:25:22Z + annotations: + kube-icinga/check_command: "check_nimble_lun" + hpe.com/docker-volume-name": "generic-nimble-24afe01f-2300-11e9-94e3-0050568fe3c2" + pv.kubernetes.io/provisioned-by: "hpe.com/nimble" + volume.beta.kubernetes.io/storage-class: "generic-nimble" +``` + ## Configuration kube-icinga itself can be configured via environment variables however you may also deploy and change the `config.json` file instead of environment variables. @@ -91,50 +154,15 @@ List of configurable values: |`kubernetes.services.ClusterIP.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_SERVICES_CLUSTERIP_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.services.ClusterIP.hostTemplates`|Specify a list of host templates|`ICINGA_SERVICES_CLUSTERIP_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.services.ClusterIP.applyServices`|URI of LDAP server|`KUBERNETES_SERVICES_CLUSTERIP_APPLYSERVICES`|`true`| -|`kubernetes.services.ClusterIP.portNameAsCommand`|Creates an icinga service with the port name as check_command. Example: If the port name is `mongodb` then the service gets checked via check_mongodb. If `false` the port protocl gets used which is usually TCP or UDP. >**Note**: It will fallback to the port protocol if the icinga plugin was not found as specified in the name. |`KUBERNETES_SERVICES_CLUSTERIP_PORTNAMEASCOMMAND`|`true`| |`kubernetes.services.NodePort.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_TYPE_DISCOVER`|`true`| |`kubernetes.services.NodePort.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_SERVICE_DEFINITION`|`{}`| |`kubernetes.services.NodePort.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_SERVICES_NODEPORT_HOST_DEFINITION`|`{}`| |`kubernetes.services.NodePort.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_SERVICES_NODEPORT_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.services.NodePort.hostTemplates`|Specify a list of host templates|`ICINGA_SERVICES_NODEPORT_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.services.NodePort.applyServices`|URI of LDAP server|`KUBERNETES_SERVICES_NODEPORT_APPLYSERVICES`|`true`| -|`kubernetes.services.NodePort.portNameAsCommand`|Creates an icinga service with the port name as check_command. Example: If the port name is `mongodb` then the service gets checked via check_mongodb. If `false` the port protocl gets used which is usually TCP or UDP. >**Note**: It will fallback to the port protocol if the icinga plugin was not found as specified in the name. |`KUBERNETES_SERVICES_NODEPORT_PORTNAMEASCOMMAND`|`true`| |`kubernetes.services.LoadBalancer.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_TYPE_DISCOVER`|`true`| |`kubernetes.services.LoadBalancer.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_SERVICE_DEFINITION`|`{}`| |`kubernetes.services.LoadBalancer.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_SERVICES_LOADBALANCER_HOST_DEFINITION`|`{}`| |`kubernetes.services.LoadBalancer.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_SERVICES_LOADBALANCER_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.services.LoadBalancer.hostTemplates`|Specify a list of host templates|`ICINGA_SERVICES_LOADBALANCER_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.services.LoadBalancer.applyServices`|URI of LDAP server|`KUBERNETES_SERVICES_LOADBALANCER_APPLYSERVICES`|`true`| -|`kubernetes.services.LoadBalancer.portNameAsCommand`|Creates an icinga service with the port name as check_command. Example: If the port name is `mongodb` then the service gets checked via check_mongodb. If `false` the port protocl gets used which is usually TCP or UDP. >**Note**: It will fallback to the port protocol if the icinga plugin was not found as specified in the name. |`KUBERNETES_SERVICES_LOADBALANCER_PORTNAMEASCOMMAND`|`true`| - -## Advanced topics - -### ClusterIP services -Ingresses, NodePort and Load Balancer services will expose your kubernetes services to the public but not ClusterIP services. ClusterIP services (Which is the default) are only internal kubernetes services and not available from outside the cluster. If your icinga2 setup lives outside the cluster you only have to options, either disable deployment for those objects or setup an icinga [satelite](https://www.icinga.com/docs/icinga2/latest/doc/06-distributed-monitoring) container which will live in kubernetes. Of course you may also deploy an entire icinga2 setup on kubernetes if you do not have an existing one. - -### Using icinga2 apply rules -You certainly can use icinga2 apply rules. You may disable auto service deployments via `applyServices` for ingresses and services and define your own services via [apply rules](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#using-apply). -Of course you can also setup a mixed deployment. Automatically let kube-icinga deploy services and apply additional services via apply rules. - ->**Note**: Since icinga apply rules are [not triggered](https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/#modifying-objects) if an object gets updated kube-icinga will delete and recreate those objects. - -All icinga host object are created with all kubernetes data available packed in the variable `vars.kubernetes`. Therefore you can apply rules with this data, for example this will create an icinga service for all objects with a kubernetes label foo=bar. - -### Overwite specific icinga object definition -It is possible to set custom options for the icinga objects during creation. You might set custom values via `kubernetes.ingresses.hostTemplates` or kubernetes.ingresses.serviceTemplate`. The same can also be done for services. For example it might be crucial to set a custom zone for ClusterIP services since they are only reachable within the cluster and shall be monitored by an icinga satelite node. Any valid setting for a specific icinga object can be set. - -```json -{ - "kubernetes": { - "services": { - "ClusterIP": - "discover": true, - "serviceDefiniton": { - "zone": "my_custom_icinga_zone" - "vars.a_custom_icinga_variable": ["foo", "bar"] - }, - } - } - } -} -``` diff --git a/config.json b/config.json index 834243d..e0a4db8 100644 --- a/config.json +++ b/config.json @@ -1,11 +1,10 @@ { "log": { - "level": "info" + "level": "debug" }, "cleanup": true, "icinga": { "address": "127.0.0.1", - "format": "${info.timestamp} ${info.level}: ${info.message}", "port": 5665, "apiUser": "kube-icinga", "apiPassword": "kube-icinga" @@ -18,7 +17,15 @@ }, "ingresses": { "discover": true, - "serviceDefiniton": {}, + "serviceDefinition": {}, + "hostDefinition": {}, + "serviceTemplates": ["generic-service"], + "hostTemplates": ["generic-host"], + "applyServices": true, + "attachToNodes": false + }, + "volumes": { + "discover": true, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], @@ -31,27 +38,24 @@ "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], - "applyServices": false, - "portNameAsCommand": true + "applyServices": false }, "NodePort": { "discover": true, - "serviceDefiniton": {}, + "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], "applyServices": false, - "portNameAsCommand": true, "attachToNodes": false }, "LoadBalancer": { "discover": true, - "serviceDefiniton": {}, + "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], - "applyServices": false, - "portNameAsCommand": true + "applyServices": false } } } diff --git a/package-lock.json b/package-lock.json index 1c9f75d..997fc61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,23 +5,31 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz", - "integrity": "sha1-vs2AVIJzREDJ0TfkbXc0DmTX9Rs=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", "dev": true, "requires": { - "@babel/highlight": "7.0.0-beta.49" + "@babel/highlight": "7.0.0" } }, "@babel/highlight": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.49.tgz", - "integrity": "sha1-lr3GtD4TSCASumaRsQGEktOWIsw=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", "dev": true, "requires": { "chalk": "2.4.1", "esutils": "2.0.2", - "js-tokens": "3.0.2" + "js-tokens": "4.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + } } }, "@sindresorhus/is": { @@ -42,9 +50,9 @@ "dev": true }, "abab": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", "dev": true }, "acorn": { @@ -54,12 +62,21 @@ "dev": true }, "acorn-globals": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", - "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", + "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", "dev": true, "requires": { - "acorn": "5.5.3" + "acorn": "6.1.0", + "acorn-walk": "6.1.1" + }, + "dependencies": { + "acorn": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", + "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", + "dev": true + } } }, "acorn-jsx": { @@ -79,6 +96,12 @@ } } }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "dev": true + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -96,23 +119,6 @@ "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", "dev": true }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, "ansi-escapes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", @@ -141,6 +147,262 @@ "normalize-path": "2.1.1" }, "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.3", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -161,12 +423,18 @@ "extglob": "2.0.4", "fragment-cache": "0.2.1", "kind-of": "6.0.2", - "nanomatch": "1.2.9", + "nanomatch": "1.2.13", "object.pick": "1.3.0", "regex-not": "1.0.2", "snapdragon": "0.8.2", "to-regex": "3.0.2" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -188,10 +456,13 @@ } }, "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } }, "arr-flatten": { "version": "1.1.0", @@ -245,9 +516,9 @@ "dev": true }, "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, "arrify": { @@ -304,9 +575,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, "aws-sign2": { @@ -375,7 +646,7 @@ "babel-traverse": "6.26.0", "babel-types": "6.26.0", "babylon": "6.18.0", - "convert-source-map": "1.5.1", + "convert-source-map": "1.6.0", "debug": "2.6.9", "json5": "0.5.1", "lodash": "4.17.10", @@ -400,12 +671,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, @@ -423,14 +688,6 @@ "lodash": "4.17.10", "source-map": "0.5.7", "trim-right": "1.0.1" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } } }, "babel-helpers": { @@ -444,13 +701,13 @@ } }, "babel-jest": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.0.1.tgz", - "integrity": "sha1-u6079SP7IC2gXtCmVAtIyE7tE6Y=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", + "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", "dev": true, "requires": { "babel-plugin-istanbul": "4.1.6", - "babel-preset-jest": "23.0.1" + "babel-preset-jest": "23.2.0" } }, "babel-messages": { @@ -470,14 +727,14 @@ "requires": { "babel-plugin-syntax-object-rest-spread": "6.13.0", "find-up": "2.1.0", - "istanbul-lib-instrument": "1.10.1", - "test-exclude": "4.2.1" + "istanbul-lib-instrument": "1.10.2", + "test-exclude": "4.2.3" } }, "babel-plugin-jest-hoist": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.0.1.tgz", - "integrity": "sha1-6qEclkVjrqnCG+zvK994U/fzwUg=", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", + "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", "dev": true }, "babel-plugin-syntax-object-rest-spread": { @@ -509,12 +766,12 @@ } }, "babel-preset-jest": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.0.1.tgz", - "integrity": "sha1-YxzFRcbPAhlDATvK8i9F2H/mIZg=", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", + "integrity": "sha1-jsegOhOPABoaj7HoETZSvxpV2kY=", "dev": true, "requires": { - "babel-plugin-jest-hoist": "23.0.1", + "babel-plugin-jest-hoist": "23.2.0", "babel-plugin-syntax-object-rest-spread": "6.13.0" } }, @@ -526,28 +783,11 @@ "requires": { "babel-core": "6.26.3", "babel-runtime": "6.26.0", - "core-js": "2.5.7", + "core-js": "2.6.4", "home-or-tmp": "2.0.0", "lodash": "4.17.10", "mkdirp": "0.5.1", "source-map-support": "0.4.18" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "0.5.7" - } - } } }, "babel-runtime": { @@ -556,7 +796,7 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.5.7", + "core-js": "2.6.4", "regenerator-runtime": "0.11.1" } }, @@ -690,6 +930,12 @@ "kind-of": "6.0.2" } }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -718,9 +964,9 @@ } }, "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz", + "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==", "dev": true }, "boom": { @@ -742,47 +988,37 @@ } }, "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", "dev": true, "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.3" } }, "browser-process-hrtime": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", - "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", "dev": true }, "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", "dev": true, "requires": { "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } } }, "bser": { @@ -800,12 +1036,6 @@ "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==", "dev": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -821,6 +1051,14 @@ "to-object-path": "0.3.0", "union-value": "1.0.0", "unset-value": "1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "cacheable-request": { @@ -860,11 +1098,10 @@ "dev": true }, "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true }, "capture-exit": { "version": "1.2.0", @@ -880,17 +1117,6 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -951,7 +1177,7 @@ "is-binary-path": "1.0.1", "is-glob": "2.0.1", "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" + "readdirp": "2.2.1" }, "dependencies": { "anymatch": { @@ -967,9 +1193,9 @@ } }, "ci-info": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", - "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", "dev": true }, "circular-json": { @@ -998,8 +1224,14 @@ "requires": { "is-descriptor": "0.1.6" } - } - } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } }, "cli-cursor": { "version": "2.1.0", @@ -1017,24 +1249,14 @@ "dev": true }, "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "dev": true, - "optional": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" } }, "clone-response": { @@ -1124,11 +1346,12 @@ "delayed-stream": "1.0.0" } }, - "compare-versions": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.2.1.tgz", - "integrity": "sha512-2y2nHcopMG/NAyk6vWXlLs86XeM9sik4jmx1tKIgzMi9/RQ2eo758RGpxQO3ErihHmg0RlQITPqgz73y6s7quA==", - "dev": true + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true, + "optional": true }, "component-emitter": { "version": "1.2.1", @@ -1155,10 +1378,13 @@ } }, "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } }, "copy-descriptor": { "version": "0.1.1", @@ -1167,9 +1393,9 @@ "dev": true }, "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.4.tgz", + "integrity": "sha512-05qQ5hXShcqGkPZpXEFLIpxayZscVD2kuMBZewxiIPPEagukO4mqgPA9CWhUvFBJfy3ODdK2p9xyHh7FTU9/7A==", "dev": true }, "core-util-is": { @@ -1211,7 +1437,7 @@ "glob2base": "0.0.12", "minimatch": "3.0.4", "mkdirp": "0.5.1", - "resolve": "1.1.7", + "resolve": "1.10.0", "safe-buffer": "5.1.2", "shell-quote": "1.6.1", "subarg": "1.0.0" @@ -1247,18 +1473,18 @@ } }, "cssom": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", + "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", "dev": true }, "cssstyle": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.3.1.tgz", - "integrity": "sha512-tNvaxM5blOnxanyxI6panOsnfiyLRj3HV4qjqqS45WPNS1usdYWRUQjqTEEELK73lpeP/1KoIGYUwrBn/VcECA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", + "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", "dev": true, "requires": { - "cssom": "0.3.2" + "cssom": "0.3.6" } }, "dashdash": { @@ -1270,14 +1496,27 @@ } }, "data-urls": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz", - "integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", "dev": true, "requires": { - "abab": "1.0.4", - "whatwg-mimetype": "2.1.0", - "whatwg-url": "6.4.1" + "abab": "2.0.0", + "whatwg-mimetype": "2.3.0", + "whatwg-url": "7.0.0" + }, + "dependencies": { + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "4.7.0", + "tr46": "1.0.1", + "webidl-conversions": "4.0.2" + } + } } }, "debug": { @@ -1332,13 +1571,12 @@ } }, "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" + "object-keys": "1.1.0" } }, "define-property": { @@ -1380,6 +1618,12 @@ "kind-of": "6.0.2" } }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -1499,36 +1743,37 @@ "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" }, "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { "is-arrayish": "0.2.1" } }, "es-abstract": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.11.0.tgz", - "integrity": "sha512-ZnQrE/lXTTQ39ulXZ+J1DTFazV9qBy61x2bY071B+qGco8Z8q1QddsLdt/EF8Ai9hcWH72dWS0kFqXLxOxqslA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", "dev": true, "requires": { - "es-to-primitive": "1.1.1", + "es-to-primitive": "1.2.0", "function-bind": "1.1.1", - "has": "1.0.1", - "is-callable": "1.1.3", - "is-regex": "1.0.4" + "has": "1.0.3", + "is-callable": "1.1.4", + "is-regex": "1.0.4", + "object-keys": "1.1.0" } }, "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "dev": true, "requires": { - "is-callable": "1.1.3", + "is-callable": "1.1.4", "is-date-object": "1.0.1", - "is-symbol": "1.0.1" + "is-symbol": "1.0.2" } }, "es6-promise": { @@ -1543,9 +1788,9 @@ "dev": true }, "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", "dev": true, "requires": { "esprima": "3.1.3", @@ -1560,6 +1805,13 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true } } }, @@ -1677,12 +1929,12 @@ "dev": true }, "exec-sh": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", - "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", + "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", "dev": true, "requires": { - "merge": "1.2.0" + "merge": "1.2.1" } }, "execa": { @@ -1707,53 +1959,12 @@ "dev": true }, "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", "dev": true, "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "is-posix-bracket": "0.1.1" } }, "expand-range": { @@ -1763,53 +1974,20 @@ "dev": true, "requires": { "fill-range": "2.2.4" - }, - "dependencies": { - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.0.0", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - } - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } } }, "expect": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.0.1.tgz", - "integrity": "sha1-mRMfL9kRVZX4zDaXQB5/BzTUX+8=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", + "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", "dev": true, "requires": { "ansi-styles": "3.2.1", - "jest-diff": "23.0.1", + "jest-diff": "23.6.0", "jest-get-type": "22.4.3", - "jest-matcher-utils": "23.0.1", - "jest-message-util": "23.0.0", - "jest-regex-util": "23.0.0" + "jest-matcher-utils": "23.6.0", + "jest-message-util": "23.4.0", + "jest-regex-util": "23.3.0" }, "dependencies": { "ansi-styles": { @@ -1818,16 +1996,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.1" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, - "requires": { - "color-name": "1.1.3" + "color-convert": "1.9.3" } } } @@ -1870,74 +2039,12 @@ } }, "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", "dev": true, "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } + "is-extglob": "1.0.0" } }, "extsprintf": { @@ -2016,26 +2123,16 @@ } }, "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "0.1.1" - } - } + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "3.1.1", + "repeat-element": "1.1.3", + "repeat-string": "1.6.1" } }, "find-index": { @@ -2080,12 +2177,6 @@ "for-in": "1.0.2" } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2127,7 +2218,7 @@ "requires": { "graceful-fs": "4.1.11", "jsonfile": "4.0.0", - "universalify": "0.1.1" + "universalify": "0.1.2" } }, "fs.realpath": { @@ -2678,9 +2769,9 @@ "dev": true }, "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, "get-stream": { @@ -2809,31 +2900,22 @@ "dev": true }, "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", + "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", "dev": true, "requires": { - "async": "1.5.2", + "async": "2.6.0", "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "source-map": "0.6.1", + "uglify-js": "3.4.9" }, "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -2852,9 +2934,9 @@ } }, "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { "function-bind": "1.1.1" @@ -2880,6 +2962,12 @@ "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-to-string-tag-x": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", @@ -2897,6 +2985,14 @@ "get-value": "2.0.6", "has-values": "1.0.0", "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "has-values": { @@ -2909,6 +3005,26 @@ "kind-of": "4.0.0" }, "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", @@ -2947,9 +3063,9 @@ } }, "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, "html-encoding-sniffer": { @@ -2958,7 +3074,7 @@ "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", "dev": true, "requires": { - "whatwg-encoding": "1.0.3" + "whatwg-encoding": "1.0.5" } }, "http-cache-semantics": { @@ -3062,7 +3178,7 @@ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { - "loose-envify": "1.3.1" + "loose-envify": "1.4.0" } }, "invert-kv": { @@ -3092,7 +3208,7 @@ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, "requires": { - "binary-extensions": "1.11.0" + "binary-extensions": "1.13.0" } }, "is-buffer": { @@ -3101,28 +3217,19 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", "dev": true }, "is-ci": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", - "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "dev": true, "requires": { - "ci-info": "1.1.3" + "ci-info": "1.6.0" } }, "is-data-descriptor": { @@ -3217,9 +3324,9 @@ } }, "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", "dev": true, "requires": { "kind-of": "3.2.2" @@ -3230,23 +3337,6 @@ "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "dev": true, - "requires": { - "is-number": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -3283,6 +3373,14 @@ "dev": true, "requires": { "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "is-posix-bracket": { @@ -3309,7 +3407,7 @@ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "has": "1.0.1" + "has": "1.0.3" } }, "is-resolvable": { @@ -3329,10 +3427,13 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "1.0.0" + } }, "is-typedarray": { "version": "1.0.0", @@ -3351,6 +3452,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3363,10 +3470,13 @@ "dev": true }, "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } }, "isstream": { "version": "0.1.2", @@ -3374,44 +3484,43 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", - "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", "dev": true, "requires": { "async": "2.6.0", - "compare-versions": "3.2.1", "fileset": "2.0.3", - "istanbul-lib-coverage": "1.2.0", - "istanbul-lib-hook": "1.2.0", - "istanbul-lib-instrument": "1.10.1", - "istanbul-lib-report": "1.1.4", - "istanbul-lib-source-maps": "1.2.4", - "istanbul-reports": "1.3.0", + "istanbul-lib-coverage": "1.2.1", + "istanbul-lib-hook": "1.2.2", + "istanbul-lib-instrument": "1.10.2", + "istanbul-lib-report": "1.1.5", + "istanbul-lib-source-maps": "1.2.6", + "istanbul-reports": "1.5.1", "js-yaml": "3.11.0", "mkdirp": "0.5.1", "once": "1.4.0" } }, "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", "dev": true }, "istanbul-lib-hook": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz", - "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", "dev": true, "requires": { "append-transform": "0.4.0" } }, "istanbul-lib-instrument": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", - "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", "dev": true, "requires": { "babel-generator": "6.26.1", @@ -3419,19 +3528,19 @@ "babel-traverse": "6.26.0", "babel-types": "6.26.0", "babylon": "6.18.0", - "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-coverage": "1.2.1", "semver": "5.5.0" } }, "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", "dev": true, "requires": { - "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-coverage": "1.2.1", "mkdirp": "0.5.1", - "path-parse": "1.0.5", + "path-parse": "1.0.6", "supports-color": "3.2.3" }, "dependencies": { @@ -3453,33 +3562,25 @@ } }, "istanbul-lib-source-maps": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz", - "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", "dev": true, "requires": { "debug": "3.1.0", - "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-coverage": "1.2.1", "mkdirp": "0.5.1", "rimraf": "2.6.2", "source-map": "0.5.7" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } } }, "istanbul-reports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", - "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", "dev": true, "requires": { - "handlebars": "4.0.11" + "handlebars": "4.1.0" } }, "isurl": { @@ -3492,19 +3593,19 @@ } }, "jest": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-23.0.1.tgz", - "integrity": "sha1-DQgykO5BEs7Pt4Dfb/gTMu03MgE=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", + "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", "dev": true, "requires": { "import-local": "1.0.0", - "jest-cli": "23.0.1" + "jest-cli": "23.6.0" }, "dependencies": { "jest-cli": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.0.1.tgz", - "integrity": "sha1-NRpbpRzyjs8gM22XowuXDR9TClY=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", + "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", "dev": true, "requires": { "ansi-escapes": "3.1.0", @@ -3513,118 +3614,121 @@ "glob": "7.1.2", "graceful-fs": "4.1.11", "import-local": "1.0.0", - "is-ci": "1.1.0", - "istanbul-api": "1.3.1", - "istanbul-lib-coverage": "1.2.0", - "istanbul-lib-instrument": "1.10.1", - "istanbul-lib-source-maps": "1.2.4", - "jest-changed-files": "23.0.1", - "jest-config": "23.0.1", - "jest-environment-jsdom": "23.0.1", + "is-ci": "1.2.1", + "istanbul-api": "1.3.7", + "istanbul-lib-coverage": "1.2.1", + "istanbul-lib-instrument": "1.10.2", + "istanbul-lib-source-maps": "1.2.6", + "jest-changed-files": "23.4.2", + "jest-config": "23.6.0", + "jest-environment-jsdom": "23.4.0", "jest-get-type": "22.4.3", - "jest-haste-map": "23.0.1", - "jest-message-util": "23.0.0", - "jest-regex-util": "23.0.0", - "jest-resolve-dependencies": "23.0.1", - "jest-runner": "23.0.1", - "jest-runtime": "23.0.1", - "jest-snapshot": "23.0.1", - "jest-util": "23.0.1", - "jest-validate": "23.0.1", - "jest-worker": "23.0.1", + "jest-haste-map": "23.6.0", + "jest-message-util": "23.4.0", + "jest-regex-util": "23.3.0", + "jest-resolve-dependencies": "23.6.0", + "jest-runner": "23.6.0", + "jest-runtime": "23.6.0", + "jest-snapshot": "23.6.0", + "jest-util": "23.4.0", + "jest-validate": "23.6.0", + "jest-watcher": "23.4.0", + "jest-worker": "23.2.0", "micromatch": "2.3.11", - "node-notifier": "5.2.1", - "realpath-native": "1.0.0", + "node-notifier": "5.4.0", + "prompts": "0.1.14", + "realpath-native": "1.1.0", "rimraf": "2.6.2", "slash": "1.0.0", "string-length": "2.0.0", "strip-ansi": "4.0.0", "which": "1.3.0", - "yargs": "11.0.0" + "yargs": "11.1.0" } } } }, "jest-changed-files": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.0.1.tgz", - "integrity": "sha1-95Vy0HIIROpd+EwqRI6GLCJU9gw=", + "version": "23.4.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.4.2.tgz", + "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", "dev": true, "requires": { "throat": "4.1.0" } }, "jest-config": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.0.1.tgz", - "integrity": "sha1-Z5i/8SR8ejkLEycZMwUAFYL8WPo=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", + "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", "dev": true, "requires": { "babel-core": "6.26.3", - "babel-jest": "23.0.1", + "babel-jest": "23.6.0", "chalk": "2.4.1", "glob": "7.1.2", - "jest-environment-jsdom": "23.0.1", - "jest-environment-node": "23.0.1", + "jest-environment-jsdom": "23.4.0", + "jest-environment-node": "23.4.0", "jest-get-type": "22.4.3", - "jest-jasmine2": "23.0.1", - "jest-regex-util": "23.0.0", - "jest-resolve": "23.0.1", - "jest-util": "23.0.1", - "jest-validate": "23.0.1", - "pretty-format": "23.0.1" + "jest-jasmine2": "23.6.0", + "jest-regex-util": "23.3.0", + "jest-resolve": "23.6.0", + "jest-util": "23.4.0", + "jest-validate": "23.6.0", + "micromatch": "2.3.11", + "pretty-format": "23.6.0" } }, "jest-diff": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.0.1.tgz", - "integrity": "sha1-PUkTfO4SwyCktNK0pvpugtSRoWo=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", "dev": true, "requires": { "chalk": "2.4.1", "diff": "3.5.0", "jest-get-type": "22.4.3", - "pretty-format": "23.0.1" + "pretty-format": "23.6.0" } }, "jest-docblock": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-23.0.1.tgz", - "integrity": "sha1-3t3RgzO+XcJBUmCgTvP86SdrVyU=", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-23.2.0.tgz", + "integrity": "sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=", "dev": true, "requires": { "detect-newline": "2.1.0" } }, "jest-each": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.0.1.tgz", - "integrity": "sha1-puXb9TCvxr+ddHkt3mnY23D4RwY=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", + "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", "dev": true, "requires": { "chalk": "2.4.1", - "pretty-format": "23.0.1" + "pretty-format": "23.6.0" } }, "jest-environment-jsdom": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.0.1.tgz", - "integrity": "sha1-2mieuTWNwW5XCKuyCPTrJqQ5V1w=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz", + "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", "dev": true, "requires": { - "jest-mock": "23.0.1", - "jest-util": "23.0.1", - "jsdom": "11.11.0" + "jest-mock": "23.2.0", + "jest-util": "23.4.0", + "jsdom": "11.12.0" } }, "jest-environment-node": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.0.1.tgz", - "integrity": "sha1-Z2t0DiBfHyvnckGWnngSvoJO55U=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.4.0.tgz", + "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", "dev": true, "requires": { - "jest-mock": "23.0.1", - "jest-util": "23.0.1" + "jest-mock": "23.2.0", + "jest-util": "23.4.0" } }, "jest-get-type": { @@ -3634,153 +3738,173 @@ "dev": true }, "jest-haste-map": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.0.1.tgz", - "integrity": "sha1-zYkFKr/Iy6AfVgu+wJ1PNq7CXU8=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", + "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", "dev": true, "requires": { "fb-watchman": "2.0.0", "graceful-fs": "4.1.11", - "jest-docblock": "23.0.1", + "invariant": "2.2.4", + "jest-docblock": "23.2.0", "jest-serializer": "23.0.1", - "jest-worker": "23.0.1", + "jest-worker": "23.2.0", "micromatch": "2.3.11", "sane": "2.5.2" } }, "jest-jasmine2": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.0.1.tgz", - "integrity": "sha1-Fth1NW5jYIcrukhCb30x/cGwvOo=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", + "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", "dev": true, "requires": { + "babel-traverse": "6.26.0", "chalk": "2.4.1", "co": "4.6.0", - "expect": "23.0.1", + "expect": "23.6.0", "is-generator-fn": "1.0.0", - "jest-diff": "23.0.1", - "jest-each": "23.0.1", - "jest-matcher-utils": "23.0.1", - "jest-message-util": "23.0.0", - "jest-snapshot": "23.0.1", - "jest-util": "23.0.1", - "pretty-format": "23.0.1" + "jest-diff": "23.6.0", + "jest-each": "23.6.0", + "jest-matcher-utils": "23.6.0", + "jest-message-util": "23.4.0", + "jest-snapshot": "23.6.0", + "jest-util": "23.4.0", + "pretty-format": "23.6.0" } }, "jest-leak-detector": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.0.1.tgz", - "integrity": "sha1-nboHUFrDSVw50+wJrB5WRZnoYaA=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", + "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", "dev": true, "requires": { - "pretty-format": "23.0.1" + "pretty-format": "23.6.0" } }, "jest-matcher-utils": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.0.1.tgz", - "integrity": "sha1-DGwNrt+YM8Kn82I2Bp7+y0w/bl8=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", + "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { "chalk": "2.4.1", "jest-get-type": "22.4.3", - "pretty-format": "23.0.1" + "pretty-format": "23.6.0" } }, "jest-message-util": { - "version": "23.0.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.0.0.tgz", - "integrity": "sha1-Bz89dscB98cYpLmvHrfxOHksR5Y=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", + "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", "dev": true, "requires": { - "@babel/code-frame": "7.0.0-beta.49", + "@babel/code-frame": "7.0.0", "chalk": "2.4.1", "micromatch": "2.3.11", "slash": "1.0.0", - "stack-utils": "1.0.1" + "stack-utils": "1.0.2" } }, "jest-mock": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-23.0.1.tgz", - "integrity": "sha1-FWn0d5aMZo/HKCc6F8N2d3O0Y1c=", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-23.2.0.tgz", + "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", "dev": true }, "jest-regex-util": { - "version": "23.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.0.0.tgz", - "integrity": "sha1-3Vwf3gxG9DcTFM8Q96dRoj9Oj3Y=", + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", + "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", "dev": true }, "jest-resolve": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.0.1.tgz", - "integrity": "sha1-P4QDRisQo0wt8dR6q1V0xJNbzSQ=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", + "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", "dev": true, "requires": { - "browser-resolve": "1.11.2", + "browser-resolve": "1.11.3", "chalk": "2.4.1", - "realpath-native": "1.0.0" + "realpath-native": "1.1.0" } }, "jest-resolve-dependencies": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.0.1.tgz", - "integrity": "sha1-0BoQ3a2RUsTOzfXqwriFccS2pk0=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", + "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", "dev": true, "requires": { - "jest-regex-util": "23.0.0", - "jest-snapshot": "23.0.1" + "jest-regex-util": "23.3.0", + "jest-snapshot": "23.6.0" } }, "jest-runner": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.0.1.tgz", - "integrity": "sha1-sXauPs+eGUqkuEp/z3DRuNsjGqc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", + "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", "dev": true, "requires": { "exit": "0.1.2", "graceful-fs": "4.1.11", - "jest-config": "23.0.1", - "jest-docblock": "23.0.1", - "jest-haste-map": "23.0.1", - "jest-jasmine2": "23.0.1", - "jest-leak-detector": "23.0.1", - "jest-message-util": "23.0.0", - "jest-runtime": "23.0.1", - "jest-util": "23.0.1", - "jest-worker": "23.0.1", - "source-map-support": "0.5.6", + "jest-config": "23.6.0", + "jest-docblock": "23.2.0", + "jest-haste-map": "23.6.0", + "jest-jasmine2": "23.6.0", + "jest-leak-detector": "23.6.0", + "jest-message-util": "23.4.0", + "jest-runtime": "23.6.0", + "jest-util": "23.4.0", + "jest-worker": "23.2.0", + "source-map-support": "0.5.10", "throat": "4.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", + "dev": true, + "requires": { + "buffer-from": "1.0.0", + "source-map": "0.6.1" + } + } } }, "jest-runtime": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.0.1.tgz", - "integrity": "sha1-sddl+wP7bUBDgFrycGdqaT9QTVc=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", + "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", "dev": true, "requires": { "babel-core": "6.26.3", "babel-plugin-istanbul": "4.1.6", "chalk": "2.4.1", - "convert-source-map": "1.5.1", + "convert-source-map": "1.6.0", "exit": "0.1.2", "fast-json-stable-stringify": "2.0.0", "graceful-fs": "4.1.11", - "jest-config": "23.0.1", - "jest-haste-map": "23.0.1", - "jest-message-util": "23.0.0", - "jest-regex-util": "23.0.0", - "jest-resolve": "23.0.1", - "jest-snapshot": "23.0.1", - "jest-util": "23.0.1", - "jest-validate": "23.0.1", + "jest-config": "23.6.0", + "jest-haste-map": "23.6.0", + "jest-message-util": "23.4.0", + "jest-regex-util": "23.3.0", + "jest-resolve": "23.6.0", + "jest-snapshot": "23.6.0", + "jest-util": "23.4.0", + "jest-validate": "23.6.0", "micromatch": "2.3.11", - "realpath-native": "1.0.0", + "realpath-native": "1.1.0", "slash": "1.0.0", "strip-bom": "3.0.0", - "write-file-atomic": "2.3.0", - "yargs": "11.0.0" + "write-file-atomic": "2.4.2", + "yargs": "11.1.0" }, "dependencies": { "strip-bom": { @@ -3798,31 +3922,36 @@ "dev": true }, "jest-snapshot": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.0.1.tgz", - "integrity": "sha1-ZnT6Gbnraamcq+zUFb3cQtavPn4=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", + "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", "dev": true, "requires": { + "babel-types": "6.26.0", "chalk": "2.4.1", - "jest-diff": "23.0.1", - "jest-matcher-utils": "23.0.1", + "jest-diff": "23.6.0", + "jest-matcher-utils": "23.6.0", + "jest-message-util": "23.4.0", + "jest-resolve": "23.6.0", "mkdirp": "0.5.1", "natural-compare": "1.4.0", - "pretty-format": "23.0.1" + "pretty-format": "23.6.0", + "semver": "5.5.0" } }, "jest-util": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.0.1.tgz", - "integrity": "sha1-aOpb1+2xd9MFn5eXJZ+ODazOL5k=", + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", + "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", "dev": true, "requires": { "callsites": "2.0.0", "chalk": "2.4.1", "graceful-fs": "4.1.11", - "is-ci": "1.1.0", - "jest-message-util": "23.0.0", + "is-ci": "1.2.1", + "jest-message-util": "23.4.0", "mkdirp": "0.5.1", + "slash": "1.0.0", "source-map": "0.6.1" }, "dependencies": { @@ -3831,25 +3960,42 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, "jest-validate": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.0.1.tgz", - "integrity": "sha1-zZ8BqJ0mu4hfEqhmdxXpyGWldU8=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", + "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", "dev": true, "requires": { "chalk": "2.4.1", "jest-get-type": "22.4.3", "leven": "2.1.0", - "pretty-format": "23.0.1" + "pretty-format": "23.6.0" + } + }, + "jest-watcher": { + "version": "23.4.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.4.0.tgz", + "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", + "dev": true, + "requires": { + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "string-length": "2.0.0" } }, "jest-worker": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.0.1.tgz", - "integrity": "sha1-nmSd2WP/QEYCb5HEAX8Dmmqkp7w=", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", + "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", "dev": true, "requires": { "merge-stream": "1.0.1" @@ -3877,37 +4023,152 @@ "optional": true }, "jsdom": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.11.0.tgz", - "integrity": "sha512-ou1VyfjwsSuWkudGxb03FotDajxAto6USAlmMZjE2lc0jCznt7sBWkhfRBRaWwbnmDqdMSTKTLT5d9sBFkkM7A==", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", "dev": true, "requires": { - "abab": "1.0.4", + "abab": "2.0.0", "acorn": "5.5.3", - "acorn-globals": "4.1.0", + "acorn-globals": "4.3.0", "array-equal": "1.0.0", - "cssom": "0.3.2", - "cssstyle": "0.3.1", - "data-urls": "1.0.0", + "cssom": "0.3.6", + "cssstyle": "1.1.1", + "data-urls": "1.1.0", "domexception": "1.0.1", - "escodegen": "1.9.1", + "escodegen": "1.11.0", "html-encoding-sniffer": "1.0.2", "left-pad": "1.3.0", - "nwsapi": "2.0.1", + "nwsapi": "2.1.0", "parse5": "4.0.0", "pn": "1.1.0", - "request": "2.86.0", + "request": "2.88.0", "request-promise-native": "1.0.5", "sax": "1.2.4", "symbol-tree": "3.2.2", "tough-cookie": "2.3.4", "w3c-hr-time": "1.0.1", "webidl-conversions": "4.0.2", - "whatwg-encoding": "1.0.3", - "whatwg-mimetype": "2.1.0", - "whatwg-url": "6.4.1", - "ws": "4.1.0", + "whatwg-encoding": "1.0.5", + "whatwg-mimetype": "2.3.0", + "whatwg-url": "6.5.0", + "ws": "5.2.2", "xml-name-validator": "3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", + "dev": true, + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" + } + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "6.9.1", + "har-schema": "2.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, + "requires": { + "mime-db": "1.37.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.8.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.2", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.1.3", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.21", + "oauth-sign": "0.9.0", + "performance-now": "2.1.0", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.4.3", + "tunnel-agent": "0.6.0", + "uuid": "3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "1.1.31", + "punycode": "1.4.1" + } + } + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } } }, "jsesc": { @@ -3996,6 +4257,12 @@ "is-buffer": "1.1.6" } }, + "kleur": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", + "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", + "dev": true + }, "kubernetes-client": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/kubernetes-client/-/kubernetes-client-5.3.0.tgz", @@ -4017,13 +4284,6 @@ "colornames": "1.1.1" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -4182,16 +4442,10 @@ "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "requires": { "js-tokens": "3.0.2" @@ -4236,9 +4490,9 @@ } }, "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", "dev": true }, "mem": { @@ -4251,9 +4505,9 @@ } }, "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, "merge-stream": { @@ -4284,52 +4538,6 @@ "object.omit": "2.0.1", "parse-glob": "3.0.4", "regex-cache": "0.4.4" - }, - "dependencies": { - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - } } }, "mime-db": { @@ -4420,9 +4628,9 @@ "optional": true }, "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, "requires": { "arr-diff": "4.0.0", @@ -4430,7 +4638,6 @@ "define-property": "2.0.2", "extend-shallow": "3.0.2", "fragment-cache": "0.2.1", - "is-odd": "2.0.0", "is-windows": "1.0.2", "kind-of": "6.0.2", "object.pick": "1.3.0", @@ -4439,6 +4646,18 @@ "to-regex": "3.0.2" }, "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -4487,27 +4706,28 @@ } }, "node-notifier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", - "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", + "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", "dev": true, "requires": { "growly": "1.3.0", + "is-wsl": "1.1.0", "semver": "5.5.0", "shellwords": "0.1.1", "which": "1.3.0" } }, "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { - "hosted-git-info": "2.6.0", - "is-builtin-module": "1.0.0", + "hosted-git-info": "2.7.1", + "resolve": "1.10.0", "semver": "5.5.0", - "validate-npm-package-license": "3.0.3" + "validate-npm-package-license": "3.0.4" } }, "normalize-path": { @@ -4545,9 +4765,9 @@ "dev": true }, "nwsapi": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.1.tgz", - "integrity": "sha512-xOJJb7kAAGy6UOklbaIPA0iu/27VMHfAbMUgYJlXz4qRXytIkPGM2vwfbxa+tbaqcqHNsP6RN4eDZlePelWKpQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz", + "integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg==", "dev": true }, "oauth-sign": { @@ -4583,9 +4803,9 @@ } }, "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", "dev": true }, "object-visit": { @@ -4595,6 +4815,14 @@ "dev": true, "requires": { "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "object.getownpropertydescriptors": { @@ -4603,8 +4831,8 @@ "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", "dev": true, "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.11.0" + "define-properties": "1.1.3", + "es-abstract": "1.13.0" } }, "object.omit": { @@ -4624,6 +4852,14 @@ "dev": true, "requires": { "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } } }, "oidc-token-hash": { @@ -4742,9 +4978,9 @@ "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=" }, "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { "p-try": "1.0.0" @@ -4756,7 +4992,7 @@ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "1.2.0" + "p-limit": "1.3.0" } }, "p-timeout": { @@ -4791,7 +5027,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.1" + "error-ex": "1.3.2" } }, "parse5": { @@ -4831,9 +5067,9 @@ "dev": true }, "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, "path-type": { @@ -4925,9 +5161,9 @@ "dev": true }, "pretty-format": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.0.1.tgz", - "integrity": "sha1-1h0GUmjkx1kIO8y8onoBrXx2AfQ=", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { "ansi-regex": "3.0.0", @@ -4946,16 +5182,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.1" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, - "requires": { - "color-name": "1.1.3" + "color-convert": "1.9.3" } } } @@ -4977,6 +5204,16 @@ "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", "dev": true }, + "prompts": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", + "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", + "dev": true, + "requires": { + "kleur": "2.0.2", + "sisteransi": "0.1.1" + } + }, "promy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/promy/-/promy-0.1.0.tgz", @@ -4987,6 +5224,12 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "dev": true + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -5008,14 +5251,14 @@ } }, "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", "dev": true, "requires": { "is-number": "4.0.0", "kind-of": "6.0.2", - "math-random": "1.0.1" + "math-random": "1.0.4" }, "dependencies": { "is-number": { @@ -5039,7 +5282,7 @@ "dev": true, "requires": { "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", + "normalize-package-data": "2.5.0", "path-type": "1.1.0" } }, @@ -5089,21 +5332,311 @@ } }, "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, "requires": { "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.6", - "set-immediate-shim": "1.0.1" + "micromatch": "3.1.10", + "readable-stream": "2.3.6" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.3", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.13", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "realpath-native": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.0.tgz", - "integrity": "sha512-XJtlRJ9jf0E1H1SLeJyQ9PGzQD7S65h1pRXEcAeK48doKOnKxcgPeNohJvD5u/2sI9J1oke6E8bZHS/fmW1UiQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", "dev": true, "requires": { "util.promisify": "1.0.0" @@ -5147,9 +5680,9 @@ "dev": true }, "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", "dev": true }, "repeat-string": { @@ -5238,10 +5771,13 @@ } }, "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "1.0.6" + } }, "resolve-cwd": { "version": "2.0.0", @@ -5296,16 +5832,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "0.1.4" - } - }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -5373,7 +5899,7 @@ "requires": { "anymatch": "2.0.0", "capture-exit": "1.2.0", - "exec-sh": "0.2.1", + "exec-sh": "0.2.2", "fb-watchman": "2.0.0", "fsevents": "1.2.4", "micromatch": "3.1.10", @@ -5382,6 +5908,262 @@ "watch": "0.18.0" }, "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.3", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -5402,7 +6184,7 @@ "extglob": "2.0.4", "fragment-cache": "0.2.1", "kind-of": "6.0.2", - "nanomatch": "1.2.9", + "nanomatch": "1.2.13", "object.pick": "1.3.0", "regex-not": "1.0.2", "snapdragon": "0.8.2", @@ -5414,6 +6196,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -5435,12 +6223,6 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", @@ -5518,6 +6300,12 @@ } } }, + "sisteransi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", + "integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==", + "dev": true + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -5546,7 +6334,7 @@ "map-cache": "0.2.2", "source-map": "0.5.7", "source-map-resolve": "0.5.2", - "use": "3.1.0" + "use": "3.1.1" }, "dependencies": { "debug": { @@ -5581,12 +6369,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true } } }, @@ -5639,6 +6421,12 @@ "kind-of": "6.0.2" } }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -5673,9 +6461,9 @@ } }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, "source-map-resolve": { @@ -5684,7 +6472,7 @@ "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", "dev": true, "requires": { - "atob": "2.1.1", + "atob": "2.1.2", "decode-uri-component": "0.2.0", "resolve-url": "0.2.1", "source-map-url": "0.4.0", @@ -5692,13 +6480,12 @@ } }, "source-map-support": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", - "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "dev": true, "requires": { - "buffer-from": "1.0.0", - "source-map": "0.6.1" + "source-map": "0.5.7" } }, "source-map-url": { @@ -5708,19 +6495,19 @@ "dev": true }, "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", "dev": true, "requires": { "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" + "spdx-license-ids": "3.0.3" } }, "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", "dev": true }, "spdx-expression-parse": { @@ -5729,14 +6516,14 @@ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "dev": true, "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" + "spdx-exceptions": "2.2.0", + "spdx-license-ids": "3.0.3" } }, "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", + "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", "dev": true }, "split-string": { @@ -5774,9 +6561,9 @@ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" }, "stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, "static-extend": { @@ -5921,45 +6708,16 @@ } }, "test-exclude": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", - "integrity": "sha512-qpqlP/8Zl+sosLxBcVKl9vYy26T9NPalxSzzCP/OY6K7j938ui2oKgo+kRZYfxAeIpLqpbVnsHq1tyV70E4lWQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", + "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", "dev": true, "requires": { "arrify": "1.0.1", - "micromatch": "3.1.10", + "micromatch": "2.3.11", "object-assign": "4.1.1", "read-pkg-up": "1.0.1", "require-main-filename": "1.0.1" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.9", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" - } - } } }, "text-hex": { @@ -6040,6 +6798,17 @@ "requires": { "is-number": "3.0.0", "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + } } }, "tough-cookie": { @@ -6093,8 +6862,8 @@ "jest-config": "22.4.4", "lodash": "4.17.10", "pkg-dir": "2.0.0", - "source-map-support": "0.5.6", - "yargs": "11.0.0" + "source-map-support": "0.5.10", + "yargs": "11.1.0" }, "dependencies": { "ansi-regex": { @@ -6109,7 +6878,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.1" + "color-convert": "1.9.3" } }, "babel-plugin-jest-hoist": { @@ -6134,15 +6903,6 @@ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, "expect": { "version": "22.4.3", "resolved": "https://registry.npmjs.org/expect/-/expect-22.4.3.tgz", @@ -6196,7 +6956,7 @@ "requires": { "jest-mock": "22.4.3", "jest-util": "22.4.3", - "jsdom": "11.11.0" + "jsdom": "11.12.0" } }, "jest-environment-node": { @@ -6225,7 +6985,7 @@ "jest-message-util": "22.4.3", "jest-snapshot": "22.4.3", "jest-util": "22.4.3", - "source-map-support": "0.5.6" + "source-map-support": "0.5.10" } }, "jest-matcher-utils": { @@ -6245,11 +7005,11 @@ "integrity": "sha512-iAMeKxhB3Se5xkSjU0NndLLCHtP4n+GtCqV0bISKA5dmOXQfEbdEmYiu2qpnWBDCQdEafNDDU6Q+l6oBMd/+BA==", "dev": true, "requires": { - "@babel/code-frame": "7.0.0-beta.49", + "@babel/code-frame": "7.0.0", "chalk": "2.4.1", "micromatch": "2.3.11", "slash": "1.0.0", - "stack-utils": "1.0.1" + "stack-utils": "1.0.2" } }, "jest-mock": { @@ -6270,7 +7030,7 @@ "integrity": "sha512-u3BkD/MQBmwrOJDzDIaxpyqTxYH+XqAXzVJP51gt29H8jpj3QgKof5GGO2uPGKGeA1yTMlpbMs1gIQ6U4vcRhw==", "dev": true, "requires": { - "browser-resolve": "1.11.2", + "browser-resolve": "1.11.3", "chalk": "2.4.1" } }, @@ -6297,7 +7057,7 @@ "callsites": "2.0.0", "chalk": "2.4.1", "graceful-fs": "4.1.11", - "is-ci": "1.1.0", + "is-ci": "1.2.1", "jest-message-util": "22.4.3", "mkdirp": "0.5.1", "source-map": "0.6.1" @@ -6325,6 +7085,22 @@ "ansi-regex": "3.0.0", "ansi-styles": "3.2.1" } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", + "dev": true, + "requires": { + "buffer-from": "1.0.0", + "source-map": "0.6.1" + } } } }, @@ -6358,9 +7134,9 @@ "dev": true }, "typescript": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.3.tgz", - "integrity": "sha512-K7g15Bb6Ra4lKf7Iq2l/I5/En+hLIHmxWZGq3D4DIRNFxMNV6j2SHSvDOqs2tGd4UvD/fJvrwopzQXjLrT7Itw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", "dev": true }, "typescript-eslint-parser": { @@ -6374,46 +7150,25 @@ } }, "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", "dev": true, "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "commander": "2.17.1", + "source-map": "0.6.1" }, "dependencies": { "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } } } }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", @@ -6450,9 +7205,9 @@ } }, "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, "unset-value": { @@ -6492,6 +7247,29 @@ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true } } }, @@ -6515,21 +7293,10 @@ "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" }, "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true }, "util-deprecate": { "version": "1.0.2", @@ -6542,7 +7309,7 @@ "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", "dev": true, "requires": { - "define-properties": "1.1.2", + "define-properties": "1.1.3", "object.getownpropertydescriptors": "2.0.3" } }, @@ -6552,12 +7319,12 @@ "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { - "spdx-correct": "3.0.0", + "spdx-correct": "3.1.0", "spdx-expression-parse": "3.0.0" } }, @@ -6577,7 +7344,7 @@ "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", "dev": true, "requires": { - "browser-process-hrtime": "0.1.2" + "browser-process-hrtime": "0.1.3" } }, "walker": { @@ -6595,7 +7362,7 @@ "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", "dev": true, "requires": { - "exec-sh": "0.2.1", + "exec-sh": "0.2.2", "minimist": "1.2.0" }, "dependencies": { @@ -6614,32 +7381,35 @@ "dev": true }, "whatwg-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", - "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", "dev": true, "requires": { - "iconv-lite": "0.4.19" + "iconv-lite": "0.4.24" }, "dependencies": { "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": "2.1.2" + } } } }, "whatwg-mimetype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", - "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", "dev": true }, "whatwg-url": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.1.tgz", - "integrity": "sha512-FwygsxsXx27x6XXuExA/ox3Ktwcbf+OAvrKmLulotDAiO1Q6ixchPFaHYsis2zZBZSJTR0+dR+JVtf7MlbqZjw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", "dev": true, "requires": { "lodash.sortby": "4.7.0", @@ -6662,13 +7432,6 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, "winston": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", @@ -6777,9 +7540,9 @@ } }, "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", + "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", "dev": true, "requires": { "graceful-fs": "4.1.11", @@ -6788,13 +7551,12 @@ } }, "ws": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", - "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", "dev": true, "requires": { - "async-limiter": "1.0.0", - "safe-buffer": "5.1.2" + "async-limiter": "1.0.0" } }, "xml-name-validator": { @@ -6815,15 +7577,15 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", - "integrity": "sha512-Rjp+lMYQOWtgqojx1dEWorjCofi1YN7AoFvYV7b1gx/7dAAeuI4kN5SZiEvr0ZmsZTOpDRcCqrpI10L31tFkBw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { "cliui": "4.1.0", "decamelize": "1.2.0", "find-up": "2.1.0", - "get-caller-file": "1.0.2", + "get-caller-file": "1.0.3", "os-locale": "2.1.0", "require-directory": "2.1.1", "require-main-filename": "1.0.1", @@ -6832,19 +7594,6 @@ "which-module": "2.0.0", "y18n": "3.2.1", "yargs-parser": "9.0.2" - }, - "dependencies": { - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "wrap-ansi": "2.1.0" - } - } } }, "yargs-parser": { @@ -6854,14 +7603,6 @@ "dev": true, "requires": { "camelcase": "4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - } } } } diff --git a/package.json b/package.json index d258246..8ab21da 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "prebuild": "tsc", "start": "tsc && nodejs build/main.js", "build": "eslint --fix src/ --ext ts && tsc && jest --coverage", - "test": "jest --coverage", + "test": "tsc && jest --coverage", "coveralls": "coveralls < coverage/lcov.info" }, "repository": { @@ -27,7 +27,7 @@ }, "homepage": "https://github.com/raffis/kube-icinga#readme", "dependencies": { - "icinga2-api": "raffis/nodejs-icinga2api", + "icinga2-api": "github:raffis/nodejs-icinga2api", "json-stream": "^1.0.0", "kubernetes-client": "^5.3.0", "winston": "^3.0.0" @@ -38,10 +38,9 @@ "coveralls": "^3.0.1", "eslint": "^4.19.1", "eslint-config-google": "^0.9.1", - "jest": "^23.0.1", - "source-map-support": "^0.5.6", + "jest": "^23.0.0", "ts-jest": "^22.4.6", - "typescript": "^2.8.3", + "typescript": "^2.9.2", "typescript-eslint-parser": "^15.0.0" }, "jest": { diff --git a/src/config.ts b/src/config.ts index cd6f3c5..1a9ab6c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,13 +10,12 @@ try { const config = { log: { level: process.env.LOG_LEVEL || defaultConfig.log.level || 'info', - format: process.env.LOG_FORMAT || defaultConfig.log.format || '${info.timestamp} ${info.level}: ${info.message}' }, cleanup: process.env.CLEANUP || defaultConfig.cleanup || true, icinga: { address: process.env.ICINGA_ADDRESS || defaultConfig.icinga.address || '127.0.0.1', port: process.env.ICINGA_PORT || defaultConfig.icinga.port || '5661', - apiUser: process.env.ICINGA_API_USER || defaultConfig.icinga.apiUser || 'admin', + apiUser: process.env.ICINGA_API_USERNAME || defaultConfig.icinga.apiUser || 'admin', apiPassword: process.env.ICINGA_API_PASSWORD || defaultConfig.icinga.apiPassword || 'admin', }, kubernetes: { @@ -34,6 +33,15 @@ const config = { hostTemplates: process.env.KUBERNETES_INGRESSES_HOST_TEMPLATES || defaultConfig.kubernetes.ingresses.hostTemplates || ['generic-host'], attachToNodes: process.env.KUBERNETES_INGRESSES_ATTACHTONODES || defaultConfig.kubernetes.ingresses.attachToNodes || false, }, + volumes: { + discover: process.env.KUBERNETES_INGRESSES_DISCOVER || defaultConfig.kubernetes.volumes.discover || true, + applyServices: process.env.KUBERNETES_INGRESSES_APPLYSERVICES || defaultConfig.kubernetes.volumes.applyServices || true, + serviceDefinition: process.env.KUBERNETES_INGRESSES_SERVICE_DEFINITION || defaultConfig.kubernetes.volumes.serviceDefinition || {}, + hostDefinition: process.env.KUBERNETES_INGRESSES_HOST_DEFINITION || defaultConfig.kubernetes.volumes.hostDefinition || {}, + serviceTemplates: process.env.KUBERNETES_INGRESSES_SERVICE_TEMPLATES || defaultConfig.kubernetes.volumes.serviceTemplates || ['generic-service'], + hostTemplates: process.env.KUBERNETES_INGRESSES_HOST_TEMPLATES || defaultConfig.kubernetes.volumes.hostTemplates || ['generic-host'], + attachToNodes: process.env.KUBERNETES_INGRESSES_ATTACHTONODES || defaultConfig.kubernetes.volumes.attachToNodes || false, + }, services: { ClusterIP: { discover: process.env.KUBERNETES_SERVICES_CLUSTERIP_DISCOVER || defaultConfig.kubernetes.services.ClusterIP.discover || false, @@ -42,7 +50,6 @@ const config = { hostDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.hostDefinition || {}, serviceTemplates: process.env.KUBERNETES_SERVICES_CLUSTERIP_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.ClusterIP.serviceTemplates || ['generic-service'], hostTemplates: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_TEMPLATES || defaultConfig.kubernetes.services.ClusterIP.hostTemplates || ['generic-host'], - portNameAsCommand: process.env.KUBERNETES_SERVICES_CLUSTERIP_PORTNAMEASCOMMAND || defaultConfig.kubernetes.services.ClusterIP.portNameAsCommand || true, }, NodePort: { discover: process.env.KUBERNETES_SERVICES_NODEPORT_DISCOVER || defaultConfig.kubernetes.services.NodePort.discover || true, @@ -51,8 +58,6 @@ const config = { hostDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_DEFINITION || defaultConfig.kubernetes.services.NodePort.hostDefinition || {}, serviceTemplates: process.env.KUBERNETES_SERVICES_NODEPORT_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.NodePort.serviceTemplates || ['generic-service'], hostTemplates: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_TEMPLATES || defaultConfig.kubernetes.services.NodePort.hostTemplates || ['generic-host'], - portNameAsCommand: process.env.KUBERNETES_SERVICES_NODEPORT_PORTNAMEASCOMMAND || defaultConfig.kubernetes.services.NodePort.portNameAsCommand || true, - attachToNodes: process.env.KUBERNETES_SERVICES_NODEPORT_ATTACHTONODES || defaultConfig.kubernetes.services.NodePort.attachToNodes || true, }, LoadBalancer: { discover: process.env.KUBERNETES_SERVICES_LOADBALANCER_DISCOVER || defaultConfig.kubernetes.services.LoadBalancer.discover || true, @@ -61,10 +66,9 @@ const config = { hostDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.hostDefinition || {}, serviceTemplates: process.env.KUBERNETES_SERVICES_LOADBALANCER_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.LoadBalancer.serviceTemplates || ['generic-service'], hostTemplates: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_TEMPLATES || defaultConfig.kubernetes.services.LoadBalancer.hostTemplates || ['generic-host'], - portNameAsCommand: process.env.KUBERNETES_SERVICES_LOADBALANCER_PORTNAMEASCOMMAND || defaultConfig.kubernetes.services.LoadBalancer.portNameAsCommand || true, }, }, }, }; -console.log(config); + export default config; diff --git a/src/icinga.ts b/src/icinga.ts index 8a58746..75a2df3 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -126,6 +126,8 @@ export default class Icinga { } else { reject(err); } + } else { + resolve(true); } }); }); @@ -207,7 +209,6 @@ export default class Icinga { return reject(err); } - console.log(result, err); var handlers = []; for (const service of result) { handlers.push(this.deleteService(service.attrs.host_name, service.attrs.name)); @@ -222,7 +223,6 @@ export default class Icinga { return reject(err); } - console.log(result); var handlers = []; for (const host of result) { handlers.push(this.deleteHost(host.attrs.name)); diff --git a/src/kube/abstract.resource.ts b/src/kube/abstract.resource.ts new file mode 100644 index 0000000..5bf78ef --- /dev/null +++ b/src/kube/abstract.resource.ts @@ -0,0 +1,55 @@ +import {Logger} from 'winston'; +import Icinga from '../icinga'; +import JSONStream from 'json-stream'; + +/** + * kubernetes hosts + */ +export default abstract class Resource { + /** + * Replace invalid icinga2 chars + */ + protected escapeName(name: string): string { + return name.replace(/\\|\//g, '-'); + } + + /** + * Prepare icinga object with kube annotations + */ + protected prepareResource(resource: any): any { + var definition: any = {}; + + if(!resource.metadata.annotations) { + return definition; + } + + var annotations: any = resource.metadata.annotations; + + if(annotations['kube-icinga/check_command']) { + definition.check_command = annotations['kube-icinga/check_command']; + } + + if(annotations['kube-icinga/definition']) { + Object.assign(definition, JSON.parse(annotations['kube-icinga/definition'])); + } + + return definition; + } + + /** + * Prepare icinga object with kube annotations + */ + protected prepareTemplates(resource: any): string[] { + if(!resource.metadata.annotations) { + return []; + } + + var annotations: any = resource.metadata.annotations; + + if(annotations['kube-icinga/templates']) { + return annotations['kube-icinga/templates'].split(','); + } + + return []; + } +} diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index 6add3d0..0dfb15e 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -2,11 +2,12 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; import JSONStream from 'json-stream'; import KubeNode from './node'; +import Resource from './abstract.resource'; /** * kubernetes ingresses */ -export default class Ingress { +export default class Ingress extends Resource { protected logger: Logger; protected kubeClient; protected icinga: Icinga; @@ -22,16 +23,17 @@ export default class Ingress { }; /** - * kubernetes ingresses + * kubernetes hosts */ - constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: object={}) { + constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + super(); this.logger = logger; this.kubeClient = kubeClient; this.icinga = icinga; this.jsonStream = jsonStream; - this.options = Object.assign(this.options, options); this.kubeNode = kubeNode; - } + this.options = Object.assign(this.options, options); + } /** * Apply host @@ -53,15 +55,15 @@ export default class Ingress { /** * Apply service */ - protected async applyService(host: string, name: string, definition) { + protected async applyService(host: string, name: string, definition, templates: string[]) { if (this.options.attachToNodes) { for (const node of this.kubeNode.getWorkerNodes()) { definition.host_name = node; - this.icinga.applyService(node, name, definition, this.options.serviceTemplates); + this.icinga.applyService(node, name, definition, templates); } } else { definition.host_name = host; - this.icinga.applyService(host, name, definition, this.options.serviceTemplates); + this.icinga.applyService(host, name, definition, templates); } } @@ -73,13 +75,17 @@ export default class Ingress { await this.applyHost(definition.metadata.name, definition.metadata.name, definition, this.options.hostTemplates); } + let service = this.prepareResource(definition); + var templates = this.options.serviceTemplates; + templates = templates.concat(this.prepareTemplates(definition)); + if (this.options.applyServices) { await this.icinga.applyServiceGroup(definition.metadata.namespace); for (const spec of definition.spec.rules) { for (const path of spec.http.paths) { let base = path.path || '/'; - let service = { + let addition = { 'check_command': 'http', 'display_name': `${spec.host}:http`, 'vars._kubernetes': true, @@ -91,14 +97,15 @@ export default class Ingress { 'groups': [definition.metadata.namespace], }; - Object.assign(service, this.options.serviceDefinition); - this.applyService(definition.metadata.name, service.display_name, service); + Object.assign(addition, this.options.serviceDefinition); + Object.assign(addition, service); + this.applyService(definition.metadata.name, addition.display_name, addition, templates); // tls secret set, also apply https service if (definition.spec.tls) { - service.display_name += 's'; - service['vars.http_ssl'] = true; - this.applyService(definition.metadata.name, service.display_name, service); + addition.display_name += 's'; + addition['vars.http_ssl'] = true; + this.applyService(definition.metadata.name, addition.display_name, addition, templates); } } } diff --git a/src/kube/node.ts b/src/kube/node.ts index 20eec5c..024f83e 100644 --- a/src/kube/node.ts +++ b/src/kube/node.ts @@ -1,11 +1,12 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; import JSONStream from 'json-stream'; +import Resource from './abstract.resource'; /** * kubernetes hosts */ -export default class Node { +export default class Node extends Resource { protected logger: Logger; protected kubeClient; protected icinga: Icinga; @@ -21,6 +22,7 @@ export default class Node { * kubernetes hosts */ constructor(logger: Logger, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + super(); this.logger = logger; this.kubeClient = kubeClient; this.icinga = icinga; diff --git a/src/main.ts b/src/main.ts index 9d23bc3..4f3c16f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,12 +6,14 @@ import config from './config'; import Node from './kube/node'; import Service from './kube/service'; import Ingress from './kube/ingress'; +import Volume from './kube/volume'; import * as JSONStream from 'json-stream'; const icinga = new IcingaWrapper(logger, icingaClient); const kubeNode = new Node(logger, kubeClient, icinga, new JSONStream(), config.kubernetes.nodes); const kubeIngress = new Ingress(logger, kubeNode, kubeClient, icinga, new JSONStream(), config.kubernetes.ingresses); const kubeService = new Service(logger, kubeNode, kubeClient, icinga, new JSONStream(), config.kubernetes.services); +const kubeVolume = new Volume(logger, kubeNode, kubeClient, icinga, new JSONStream(), config.kubernetes.volumes); /** * Main @@ -24,17 +26,21 @@ async function main() { } if (config.kubernetes.nodes.discover) { - kubeNode.kubeListener(); + // kubeNode.kubeListener(); } if (config.kubernetes.ingresses.discover) { - kubeIngress.kubeListener(); + // kubeIngress.kubeListener(); + } + + if (config.kubernetes.volumes.discover) { + kubeVolume.kubeListener(); } if (config.kubernetes.services.ClusterIP.discover || config.kubernetes.services.NodePort.discover || config.kubernetes.services.LoadBalancer.discover) { - kubeService.kubeListener(); + // kubeService.kubeListener(); } } diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index f1b88f7..9c0958a 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -9,18 +9,13 @@ jest.mock('../../src/kube/node'); jest.mock('json-stream'); jest.mock('kubernetes-client'); -const fixture = { +const template = { "apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": { "annotations": {}, - "creationTimestamp": "2018-02-06T15:30:59Z", - "generation": 4, "name": "foo", "namespace": "foobar", - "resourceVersion": "21173149", - "selfLink": "/apis/extensions/v1beta1/namespaces/foobar/ingresses/foo", - "uid": "bba7f53c-0b52-11e8-a755-0050568fe3c2" }, "spec": { "rules": [ @@ -57,40 +52,12 @@ const fixture = { "loadBalancer": {} } }; -const tlsFixture = { - "apiVersion": "extensions/v1beta1", - "kind": "Ingress", - "metadata": { - "annotations": {}, - "creationTimestamp": "2018-02-06T15:30:59Z", - "generation": 4, - "name": "foo", - "namespace": "foobar", - "resourceVersion": "21173149", - "selfLink": "/apis/extensions/v1beta1/namespaces/foobar/ingresses/foo", - "uid": "bba7f53c-0b52-11e8-a755-0050568fe3c2" - }, - "spec": { - "rules": [ - { - "host": "barfoo.example.org", - "http": { - "paths": [ - { - "backend": { - "serviceName": "foo-backend", - "servicePort": 80 - } - } - ] - } - } - ] - "tls": { - "secretName": "foo" - } - }, -}; + +var fixture; + +beforeEach(() => { + fixture = JSON.parse(JSON.stringify(template)); +}); describe('kubernetes ingresses', () => { var instance: Ingress; @@ -223,13 +190,72 @@ describe('kubernetes ingresses', () => { serviceTemplates: ['foo', 'bar'] }); + fixture.spec.tls = { + secretName: 'foo' + } + Icinga.applyService = jest.fn(); - await instance.prepareObject(tlsFixture); + await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; - expect(Icinga.applyService.mock.instances.length).toBe(2); - expect(calls[0][1]).toBe('barfoo.example.org:http'); - expect(calls[1][1]).toBe('barfoo.example.org:https'); + expect(Icinga.applyService.mock.instances.length).toBe(4); + expect(calls[0][1]).toBe('foobar.example.org:http'); + expect(calls[1][1]).toBe('foobar.example.org:https'); expect(calls[1][2]['vars.http_ssl']).toBe(true); }); + + }); + + describe('kubernetes annotations', () => { + it('check_command/templates annotation', async () => { + let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + applyServices: true + }); + + fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + fixture.metadata.annotations['kube-icinga/templates'] = 'foobar,barfoo'; + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2].check_command).toBe('bar'); + expect(calls[1][2].check_command).toBe('bar'); + expect(calls[0][3]).toEqual(['foobar', 'barfoo']); + expect(calls[1][3]).toEqual(['foobar', 'barfoo']); + }); + + it('use annotation instead global definition', async () => { + let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + applyServices: true, + serviceDefinition: { + check_command: 'foo' + } + }); + fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2].check_command).toBe('bar'); + expect(calls[1][2].check_command).toBe('bar'); + }); + + it('definiton merge', async () => { + let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + applyServices: true, + serviceDefinition: { + check_command: 'foo' + } + }); + fixture.metadata.annotations['kube-icinga/definition'] = '{"vars.foo": "foobar"}'; + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2]["check_command"]).toBe('foo'); + expect(calls[0][2]["vars.foo"]).toBe('foobar'); + }); }); }); diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts new file mode 100644 index 0000000..710805f --- /dev/null +++ b/tests/kube/service.test.ts @@ -0,0 +1,294 @@ +import Service from '../../src/kube/service'; +import Node from '../../src/kube/node'; +import Icinga from '../../src/icinga'; +import Logger from '../../src/logger'; +import * as JSONStream from 'json-stream'; +const KubeApi = require('kubernetes-client').Client; +jest.mock('../../src/icinga'); +jest.mock('../../src/kube/node'); +jest.mock('json-stream'); +jest.mock('kubernetes-client'); +jest.mock('../../src/logger'); + +const template = { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "name": "foo", + "namespace": "foobar", + "annotations": {} + }, + "spec": { + "clusterIP": "10.99.24.32", + "ports": [ + { + "name": "http", + "port": 80, + "protocol": "TCP", + "targetPort": 80 + }, + { + "name": "bar", + "port": 10000, + "protocol": "tcp", + "targetPort": 10000 + } + ], + "selector": { + "app": "bar" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } +}; + +var fixture; + +beforeEach(() => { + fixture = JSON.parse(JSON.stringify(template)); +}); + +describe('kubernetes services', () => { + var instance: Service; + + describe('add service object with dummy host', () => { + it('create icinga host object', () => { + let instance = new Service(Logger, Node, KubeApi, Icinga); + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('foo'); + expect(call[1]).toBe('10.99.24.32'); + expect(call[2].display_name).toBe('foo'); + expect(call[2].check_command).toBe('dummy'); + }); + + it('create icinga host object with custom definitions', () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true, + hostDefinition: { + 'vars.foo': 'bar', + 'vars.check_command': 'foo' + } + } + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + console.log(Icinga.applyHost.mock.calls); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[2]['vars.foo']).toBe('bar'); + expect(call[2]['vars.check_command']).toBe('foo'); + }); + + it('create icinga host object with templates', () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + hostTemplates: ['foo', 'bar'] + } + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[3]).toEqual(['foo', 'bar']); + }); + + it('do not create icinga host object while service is of type NodePort', () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream); + + fixture.spec.type = 'NodePort'; + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + expect(Icinga.applyHost.mock.instances.length).toBe(0); + }); + }); + + describe('add service object namespace as service group', () => { + it('create service group per default', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream); + + Icinga.applyHost = jest.fn(); + Icinga.hasCheckCommand = jest.fn(); + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + await instance.prepareObject(fixture); + const call = Icinga.applyServiceGroup.mock.calls[0]; + expect(call[0]).toBe('foobar'); + }); + + it('do not create servicegroup if applyServices is disabled', () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + applyServices: false + }); + + Icinga.applyServiceGroup = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyServiceGroup.mock.instances[0]; + expect(Icinga.applyServiceGroup.mock.instances.length).toBe(0); + }); + }); + + describe('add all service object ports as service objects', () => { + it('create all service objects', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream); + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + console.log(calls); + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][0]).toBe('foo'); + expect(calls[1][0]).toBe('foo'); + expect(calls[0][1]).toBe('http'); + expect(calls[1][1]).toBe('bar'); + + expect(calls[0][2]['check_command']).toBe('tcp'); + expect(calls[0][2]['vars.tcp_port']).toBe(80); + expect(calls[1][2]['check_command']).toBe('tcp'); + expect(calls[1][2]['vars.tcp_port']).toBe(10000); + }); + + it('create all service objects with custom service definition', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true, + serviceDefinition: { + 'check_command': 'http', + 'vars.foo': 'bar' + } + } + }); + + Icinga.applyHost = jest.fn(); + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + Icinga.hasCheckCommand = jest.fn(); + Icinga.hasCheckCommand.mockResolvedValue(true); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2].check_command).toBe('http'); + expect(calls[0][2]['vars.foo']).toBe('bar'); + expect(calls[1][2].check_command).toBe('http'); + expect(calls[1][2]['vars.foo']).toBe('bar'); + }); + + it('create all service objects with custom service definition, check_command not found, fallback to protocol', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true, + serviceDefinition: { + 'check_command': 'http', + 'vars.foo': 'bar' + } + } + }); + + Icinga.applyHost = jest.fn(); + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + Icinga.hasCheckCommand = jest.fn(); + Icinga.hasCheckCommand.mockResolvedValue(false); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2].check_command).toBe('tcp'); + expect(calls[0][2]['vars.foo']).toBe('bar'); + expect(calls[1][2].check_command).toBe('tcp'); + expect(calls[1][2]['vars.foo']).toBe('bar'); + }); + + it('create all service objects with templates', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true, + serviceTemplates: ['foo', 'bar'] + } + }); + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][3]).toEqual(['foo', 'bar']); + expect(calls[1][3]).toEqual(['foo', 'bar']); + }); + }); + + describe('kubernetes annotations', () => { + it('check_command/templates annotation', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true + } + }); + + fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + fixture.metadata.annotations['kube-icinga/templates'] = 'foobar,barfoo'; + Icinga.applyService = jest.fn(); + Icinga.hasCheckCommand = jest.fn(); + Icinga.hasCheckCommand.mockResolvedValue(true); + Icinga.applyHost = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2].check_command).toBe('bar'); + expect(calls[1][2].check_command).toBe('bar'); + expect(calls[0][3]).toEqual(['foobar', 'barfoo']); + expect(calls[1][3]).toEqual(['foobar', 'barfoo']); + }); + + it('use annotation instead global definition', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true, + serviceDefinition: { + check_command: 'foo' + } + } + }); + fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + + Icinga.applyService = jest.fn(); + Icinga.hasCheckCommand = jest.fn(); + Icinga.hasCheckCommand.mockResolvedValue(true); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2].check_command).toBe('bar'); + expect(calls[1][2].check_command).toBe('bar'); + }); + + it('definiton merge', async () => { + let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + ClusterIP: { + applyServices: true, + serviceDefinition: { + check_command: 'foo' + } + } + }); + fixture.metadata.annotations['kube-icinga/definition'] = '{"vars.foo": "foobar"}'; + + Icinga.applyService = jest.fn(); + Icinga.hasCheckCommand = jest.fn(); + Icinga.hasCheckCommand.mockResolvedValue(true); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][2]["check_command"]).toBe('foo'); + expect(calls[0][2]["vars.foo"]).toBe('foobar'); + }); + }); +}); From 076daad14986f641142161f28c9c350b4215d8fd Mon Sep 17 00:00:00 2001 From: raffis Date: Wed, 13 Feb 2019 10:42:46 +0100 Subject: [PATCH 05/15] added new tests --- src/icinga.ts | 58 ++++--- src/kube/ingress.ts | 4 +- src/kube/node.ts | 4 +- src/kube/service.ts | 4 +- src/kube/volume.ts | 4 +- tests/icinga.test.ts | 333 +++++++++++++++++++++++++++++++++++++ tests/kube/ingress.test.ts | 11 +- tests/kube/service.test.ts | 12 +- 8 files changed, 385 insertions(+), 45 deletions(-) create mode 100644 tests/icinga.test.ts diff --git a/src/icinga.ts b/src/icinga.ts index 75a2df3..7175a4f 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -38,7 +38,7 @@ export default class Icinga { /** * Create host group */ - public applyHostGroup(name: string): Promise { + public applyHostGroup(name: string): Promise { return new Promise((resolve, reject) => { this.logger.info(`apply host group ${name} aka kubernetes namespace`); @@ -48,19 +48,19 @@ export default class Icinga { this.logger.info(`host group ${name} on monitoring was not found, create one`, {error: err}); this.icingaClient.createHostGroup(name, name, [], (err, result) => { - resolve(); - if (err) { this.logger.error(`failed create host group ${name}`, {error: err}); + reject(err); } else { this.logger.info(`host group ${name} was created successfully`, {result: result}); + resolve(true); } }); } else { - reject(); + reject(err); } } else { - resolve(); + resolve(true); } }); }); @@ -79,16 +79,16 @@ export default class Icinga { this.logger.info(`service group ${name} on monitoring was not found, create one`, {error: err}); this.icingaClient.createServiceGroup(name, name, [], (err, result) => { - resolve(true); - if (err) { this.logger.error(`failed create service group ${name}`, {error: err}); + reject(err); } else { this.logger.info(`service group ${name} was created successfully`, {result: result}); + resolve(true); } }); } else { - reject(); + reject(err); } } else { resolve(true); @@ -100,7 +100,7 @@ export default class Icinga { /** * Create or update a new icinga host object */ - public applyHost(name: string, address: string, definition, templates: string[]=[]): Promise { + public applyHost(name: string, definition, templates: string[]=[]): Promise { let host = { attrs: definition, templates: templates, @@ -115,12 +115,12 @@ export default class Icinga { this.logger.info(`host ${name} on monitoring was not found, create one`, {error: err}); this.icingaClient.createHostCustom(JSON.stringify(host), name, (err, result) => { - resolve(true); - if (err) { this.logger.error(`failed create host ${name}`, {error: err}); + reject(err); } else { this.logger.info(`host ${name} was created successfully`, {result: result}); + resolve(true); } }); } else { @@ -136,28 +136,36 @@ export default class Icinga { /** * Create or update an icinga service object */ - public applyService(host: string, name: string, definition, templates: string[]=[]) { + public applyService(host: string, name: string, definition, templates: string[]=[]): Promise { let service = { attrs: definition, templates: templates, }; - this.logger.info(`apply service ${name} to host ${host}`, {service: definition}); + return new Promise((resolve, reject) => { + this.logger.info(`apply service ${name} to host ${host}`, {service: definition}); - this.icingaClient.getService(host, name, (err, result) => { - if (err) { - if (err.Statuscode == '404') { - this.logger.info(`service ${name} on host ${host} was not found, create one`, {error: err}); + this.icingaClient.getService(host, name, (err, result) => { + if (err) { + if (err.Statuscode == '404') { + this.logger.info(`service ${name} on host ${host} was not found, create one`, {error: err}); - this.icingaClient.createServiceCustom(JSON.stringify(service), host, name, (err, result) => { - if (err) { - this.logger.error(`failed create service ${name} on host ${host}`, {error: err}); - } else { - this.logger.info(`service ${name} on host ${host} was created successfully`, {result: result}); - } - }); + this.icingaClient.createServiceCustom(JSON.stringify(service), host, name, (err, result) => { + if (err) { + this.logger.error(`failed create service ${name} on host ${host}`, {error: err}); + reject(err); + } else { + this.logger.info(`service ${name} on host ${host} was created successfully`, {result: result}); + resolve(true); + } + }); + } else { + reject(err); + } + } else { + resolve(true); } - } + }); }); } diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index 0dfb15e..0486c8a 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -38,7 +38,7 @@ export default class Ingress extends Resource { /** * Apply host */ - protected async applyHost(name: string, address: string, metadata, templates: string[]) { + protected async applyHost(name: string, address: string, metadata, templates: string[]): Promise { let definition = { 'display_name': name, 'address': address, @@ -49,7 +49,7 @@ export default class Ingress extends Resource { }; Object.assign(definition, this.options.hostDefinition); - return this.icinga.applyHost(name, address, definition, this.options.hostTemplates); + return this.icinga.applyHost(name, definition, this.options.hostTemplates); } /** diff --git a/src/kube/node.ts b/src/kube/node.ts index 024f83e..a7d9167 100644 --- a/src/kube/node.ts +++ b/src/kube/node.ts @@ -33,7 +33,7 @@ export default class Node extends Resource { /** * Preapre icinga object and apply */ - protected async prepareObject(definition: any): Promise { + protected async prepareObject(definition: any): Promise { let host = { 'display_name': definition.metadata.name, 'host_name': definition.metadata.name, @@ -48,7 +48,7 @@ export default class Node extends Resource { } host = Object.assign(host, this.options.hostDefinition); - return this.icinga.applyHost(host.host_name, host.host_name, host, this.options.hostTemplates); + return this.icinga.applyHost(host.host_name, host, this.options.hostTemplates); } /** diff --git a/src/kube/service.ts b/src/kube/service.ts index e3ed7f2..48d4fec 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -81,7 +81,7 @@ export default class Service extends Resource { /** * Apply host */ - protected async applyHost(name: string, address: string, type: string, metadata, templates: string[]) { + protected async applyHost(name: string, address: string, type: string, metadata, templates: string[]): Promise { let definition = { 'display_name': name, 'address': address, @@ -92,7 +92,7 @@ export default class Service extends Resource { }; Object.assign(definition, this.options[type].hostDefinition); - return this.icinga.applyHost(name, address, definition, templates); + return this.icinga.applyHost(name, definition, templates); } /** diff --git a/src/kube/volume.ts b/src/kube/volume.ts index e6c86b5..01c675c 100644 --- a/src/kube/volume.ts +++ b/src/kube/volume.ts @@ -38,7 +38,7 @@ export default class Volume extends Resource { /** * Apply host */ - protected async applyHost(name: string, address: string, metadata, templates: string[]) { + protected async applyHost(name: string, address: string, metadata, templates: string[]): Promise { let definition = { 'display_name': name, 'address': address, @@ -49,7 +49,7 @@ export default class Volume extends Resource { }; Object.assign(definition, this.options.hostDefinition); - return this.icinga.applyHost(name, address, definition, this.options.hostTemplates); + return this.icinga.applyHost(name, definition, this.options.hostTemplates); } /** diff --git a/tests/icinga.test.ts b/tests/icinga.test.ts new file mode 100644 index 0000000..ffed4c0 --- /dev/null +++ b/tests/icinga.test.ts @@ -0,0 +1,333 @@ +/* + public hasCheckCommand(command: string): Promise { + public applyHostGroup(name: string): Promise { + public applyServiceGroup(name: string): Promise { + public applyHost(name: string, address: string, definition, templates: string[]=[]): Promise { + public applyService(host: string, name: string, definition, templates: string[]=[]) { + public deleteService(host: string, name: string): Promise { + public deleteHost(name: string): Promise { + public async cleanup(): Promise { +*/ +import * as IcingaApi from 'icinga2-api'; +import IcingaClient from '../../src/icinga'; +import Logger from '../../src/logger'; +jest.mock('icinga2-api'); +jest.mock('../../src/logger'); +var icinga; + +beforeEach(() => { + icinga = new IcingaClient(Logger, IcingaApi); +}); + +describe('icinga', () => { + describe('check_command', () => { + it('icinga check_command does not exists', async () => { + IcingaApi.getCheckCommand = jest.fn() + .mockImplementation((command, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.hasCheckCommand('foobar')).resolves.toEqual(false); + const calls = IcingaApi.getCheckCommand.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + + it('icinga check_command does exists', async () => { + IcingaApi.getCheckCommand = jest.fn() + .mockImplementation((command, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.hasCheckCommand('foobar')).resolves.toEqual(true); + const calls = IcingaApi.getCheckCommand.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + + it('icinga check_command error response', async () => { + IcingaApi.getCheckCommand = jest.fn() + .mockImplementation((command, cb) => cb({Statuscode: 500}, null)); + + var result = await expect(icinga.hasCheckCommand('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + + const calls = IcingaApi.getCheckCommand.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + + describe('apply host group', () => { + it('icinga host group does exists', async () => { + IcingaApi.createHostGroup = jest.fn(); + IcingaApi.getHostGroup = jest.fn() + .mockImplementation((hostgroup, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.applyHostGroup('foobar')).resolves.toEqual(true); + const calls = IcingaApi.getHostGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createHostGroup.mock.calls.length).toBe(0); + }); + + it('get host group error response', async () => { + IcingaApi.createHostGroup = jest.fn(); + IcingaApi.getHostGroup = jest.fn() + .mockImplementation((hostgroup, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.applyHostGroup('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + const calls = IcingaApi.getHostGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createHostGroup.mock.calls.length).toBe(0); + }); + + it('add new host group', async () => { + IcingaApi.createHostGroup = jest.fn() + .mockImplementation((hostgroup, display_name, data, cb) => cb(null, {foo: "bar"})); + + IcingaApi.getHostGroup = jest.fn() + .mockImplementation((hostgroup, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyHostGroup('foobar')).resolves.toEqual(true); + var calls = IcingaApi.getHostGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createHostGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + + it('add new host group error response', async () => { + IcingaApi.createHostGroup = jest.fn() + .mockImplementation((hostgroup, display_name, data, cb) => cb({Statuscode: 500}, null)); + + IcingaApi.getHostGroup = jest.fn() + .mockImplementation((hostgroup, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyHostGroup('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + + var calls = IcingaApi.getHostGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createHostGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + }); + + describe('apply service group', () => { + it('icinga service group does exists', async () => { + IcingaApi.createServiceGroup = jest.fn(); + IcingaApi.getServiceGroup = jest.fn() + .mockImplementation((ServiceGroup, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.applyServiceGroup('foobar')).resolves.toEqual(true); + const calls = IcingaApi.getServiceGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createServiceGroup.mock.calls.length).toBe(0); + }); + + it('get service group error response', async () => { + IcingaApi.createServiceGroup = jest.fn(); + IcingaApi.getServiceGroup = jest.fn() + .mockImplementation((ServiceGroup, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.applyServiceGroup('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + const calls = IcingaApi.getServiceGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createServiceGroup.mock.calls.length).toBe(0); + }); + + it('add new service group', async () => { + IcingaApi.createServiceGroup = jest.fn() + .mockImplementation((ServiceGroup, display_name, data, cb) => cb(null, {foo: "bar"})); + + IcingaApi.getServiceGroup = jest.fn() + .mockImplementation((ServiceGroup, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyServiceGroup('foobar')).resolves.toEqual(true); + var calls = IcingaApi.getServiceGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createServiceGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + + it('add new service group error response', async () => { + IcingaApi.createServiceGroup = jest.fn() + .mockImplementation((ServiceGroup, display_name, data, cb) => cb({Statuscode: 500}, null)); + + IcingaApi.getServiceGroup = jest.fn() + .mockImplementation((ServiceGroup, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyServiceGroup('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + + var calls = IcingaApi.getServiceGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createServiceGroup.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + }); + }); + + describe('apply host', () => { + it('icinga host does exists', async () => { + IcingaApi.createHostCustom = jest.fn(); + IcingaApi.getHostState = jest.fn() + .mockImplementation((Host, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.applyHost('foobar')).resolves.toEqual(true); + const calls = IcingaApi.getHostState.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createHostCustom.mock.calls.length).toBe(0); + }); + + it('get host error response', async () => { + IcingaApi.createHostCustom = jest.fn(); + IcingaApi.getHostState = jest.fn() + .mockImplementation((Host, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.applyHost('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + const calls = IcingaApi.getHostState.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createHostCustom.mock.calls.length).toBe(0); + }); + + it('add new host', async () => { + IcingaApi.createHostCustom = jest.fn() + .mockImplementation((data, name, cb) => cb(null, {foo: "bar"})); + + IcingaApi.getHostState = jest.fn() + .mockImplementation((Host, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyHost('foobar', {foo: "bar"}, ["foobar"])).resolves.toEqual(true); + var calls = IcingaApi.getHostState.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createHostCustom.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe(JSON.stringify({ + attrs: { + foo: "bar" + }, + templates: ["foobar"] + })); + + expect(calls[0][1]).toBe('foobar'); + }); + + it('add new host error response', async () => { + IcingaApi.createHostCustom = jest.fn() + .mockImplementation((data, name, cb) => cb({Statuscode: 500}, null)); + + IcingaApi.getHostState = jest.fn() + .mockImplementation((Host, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyHost('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + + var calls = IcingaApi.getHostState.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createHostCustom.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][1]).toBe('foobar'); + }); + }); + + describe('apply service', () => { + it('icinga service does exists', async () => { + IcingaApi.createServiceCustom = jest.fn(); + IcingaApi.getService = jest.fn() + .mockImplementation((host, service, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.applyService('foobar')).resolves.toEqual(true); + const calls = IcingaApi.getService.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createServiceCustom.mock.calls.length).toBe(0); + }); + + it('get service error response', async () => { + IcingaApi.createServiceCustom = jest.fn(); + IcingaApi.getService = jest.fn() + .mockImplementation((host, service, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.applyService('foobar')).rejects.toEqual({ + Statuscode: 500 + }); + const calls = IcingaApi.getService.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(IcingaApi.createServiceCustom.mock.calls.length).toBe(0); + }); + + it('add new service', async () => { + IcingaApi.createServiceCustom = jest.fn() + .mockImplementation((data, host, name, cb) => cb(null, {foo: "bar"})); + + IcingaApi.getService = jest.fn() + .mockImplementation((host, service, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyService('foobar', 'bar', {foo: "bar"}, ["foobar"])).resolves.toEqual(true); + var calls = IcingaApi.getService.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createServiceCustom.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe(JSON.stringify({ + attrs: { + foo: "bar" + }, + templates: ["foobar"] + })); + + expect(calls[0][1]).toBe('foobar'); + expect(calls[0][2]).toBe('bar'); + }); + + it('add new service error response', async () => { + IcingaApi.createServiceCustom = jest.fn() + .mockImplementation((data, host, name, cb) => cb({Statuscode: 500}, null)); + + IcingaApi.getService = jest.fn() + .mockImplementation((host, service, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyService('foobar', 'bar')).rejects.toEqual({ + Statuscode: 500 + }); + + var calls = IcingaApi.getService.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + expect(calls[0][1]).toBe('bar'); + + calls = IcingaApi.createServiceCustom.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][1]).toBe('foobar'); + expect(calls[0][2]).toBe('bar'); + }); + }); +}); diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index 9c0958a..9de336e 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -69,9 +69,8 @@ describe('kubernetes ingresses', () => { instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; expect(call[0]).toBe('foo'); - expect(call[1]).toBe('foo'); - expect(call[2].display_name).toBe('foo'); - expect(call[2].check_command).toBe('dummy'); + expect(call[1].display_name).toBe('foo'); + expect(call[1].check_command).toBe('dummy'); }); it('create icinga host object with custom definitions', () => { @@ -85,8 +84,8 @@ describe('kubernetes ingresses', () => { Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; - expect(call[2]['vars.foo']).toBe('bar'); - expect(call[2]['vars.check_command']).toBe('foo'); + expect(call[1]['vars.foo']).toBe('bar'); + expect(call[1]['vars.check_command']).toBe('foo'); }); it('create icinga host object with templates', () => { @@ -97,7 +96,7 @@ describe('kubernetes ingresses', () => { Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; - expect(call[3]).toEqual(['foo', 'bar']); + expect(call[2]).toEqual(['foo', 'bar']); }); it('do not create icinga host object while attachToNodes is enabled', () => { diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts index 710805f..f5f583d 100644 --- a/tests/kube/service.test.ts +++ b/tests/kube/service.test.ts @@ -61,9 +61,9 @@ describe('kubernetes services', () => { instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; expect(call[0]).toBe('foo'); - expect(call[1]).toBe('10.99.24.32'); - expect(call[2].display_name).toBe('foo'); - expect(call[2].check_command).toBe('dummy'); + expect(call[1].address).toBe('10.99.24.32'); + expect(call[1].display_name).toBe('foo'); + expect(call[1].check_command).toBe('dummy'); }); it('create icinga host object with custom definitions', () => { @@ -81,8 +81,8 @@ describe('kubernetes services', () => { instance.prepareObject(fixture); console.log(Icinga.applyHost.mock.calls); const call = Icinga.applyHost.mock.calls[0]; - expect(call[2]['vars.foo']).toBe('bar'); - expect(call[2]['vars.check_command']).toBe('foo'); + expect(call[1]['vars.foo']).toBe('bar'); + expect(call[1]['vars.check_command']).toBe('foo'); }); it('create icinga host object with templates', () => { @@ -95,7 +95,7 @@ describe('kubernetes services', () => { Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; - expect(call[3]).toEqual(['foo', 'bar']); + expect(call[2]).toEqual(['foo', 'bar']); }); it('do not create icinga host object while service is of type NodePort', () => { From 77f224abc59fc7a07a1e744d513bb1d1c12f0915 Mon Sep 17 00:00:00 2001 From: raffis Date: Wed, 13 Feb 2019 16:58:21 +0100 Subject: [PATCH 06/15] various fixes --- CHANGELOG.md | 4 ++ README.md | 35 ++++++++++++-- config.json | 2 +- docker-compose-dev.yml | 7 ++- src/icinga.ts | 6 ++- src/kube/ingress.ts | 31 ++++++++---- src/kube/node.ts | 32 +++++++------ src/kube/service.ts | 27 +++++------ src/kube/volume.ts | 28 +++++++---- src/main.ts | 26 ++++++---- tests/icinga.test.ts | 52 ++++++++++++++++++-- tests/kube/ingress.test.ts | 53 ++++++++++++--------- tests/kube/node.test.ts | 98 ++++++++++++++++++++++++++++++++++++++ tests/kube/service.test.ts | 64 ++++++++++++++----------- 14 files changed, 344 insertions(+), 121 deletions(-) create mode 100644 tests/kube/node.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a92b12d..c39411e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ * [BREAKER!] Removed service.portNameAsCommand option, use kubernetes annotations * [FEATURE] Discover persistent volumes and create icinga services for those #4 * [FIX] Fixed typo Definiton => Definition in default config example +* [CHANGE] Added various new tests +* [FIX] kube workes are in no namespace, remove icinga groups +* [FIX] fixed Error: Invalid attribute specified: host_name\n for kube nodes +* [FIX] fixed duplicate ingress objects (different path, same ingress names in different namespaces, ...) ## 1.0.1 diff --git a/README.md b/README.md index 4ee98cb..c312ebc 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,16 @@ kube-icinga automatically deploys icinga objects out of your kubernetes resource It has built-in autodiscovery and will work out of the box. However you can change various default configurations and deploy custom icinga objects or disable/enable kubernetes objects to monitor. +## Features + +* Autodiscovery +* Icinga servicegroup support +* Create services for kubernetes nodes, services, ingresses and persistent volumes +* Completely customizable per resurce or per resource type + ## How does it work? -Multiple watchers are bootstraped and listen for any kubernetes changes. Those changes will reflect immediately on icinga. +Multiple watchers are bootstraped and listen for any kubernetes changes. Those changes will reflect immediately on your icinga environment. * Kubernetes namespaces will result in icinga service groups and host groups * Nodes will result in host objects @@ -19,6 +26,7 @@ Multiple watchers are bootstraped and listen for any kubernetes changes. Those c * Persistent volumes will result in services attached to a dummy host >**Note**: [1] You may change this behaviour by attaching the services (ingress paths) to all kubernetes worker nodes by setting `kubernetes.ingresses.attachToNodes` to `true`. (This will result in many more services and checks depending on the size of your cluster!) + >**Note**: [2] NodePort services will always be attached to each kubernetes worker node. See [NodePort](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) services for more information. Since there is no such thing as hosts in the world of moving and containers. The host object (kubernetes metadata.name) on icinga may just be a [dummy](https://www.icinga.com/docs/icinga2/latest/doc/10-icinga-template-library/#plugin-check-command-dummy) and will not get checked @@ -62,19 +70,34 @@ kubectl -f https://raw.githubusercontent.com/gyselroth/kube-icinga/master/kube-i ``` (Change the secret password and ICINGA_ADDRESS value accordingly) +>**Note**: kube-icinga will be created as single pod deployment in the kubernetes kube-system namespace. You may changes this behaviour. + +### Resource visibility +The resource yaml also contains a new cluster role `kube-icinga`. kube-icinga will create icinga objects for all visible namespaces which are by default all namespaces since it is a kubernetes cluster role. +You may specify resources visibile to kube-icinga with custom RBAC rules. ## Advanced topics ### ClusterIP services -Ingresses, NodePort and Load Balancer services will expose your kubernetes services to the public but not ClusterIP services. ClusterIP services (Which is the default) are only internal kubernetes services and not available from outside the cluster. If your icinga2 setup lives outside the cluster you only have to options, either disable deployment for those objects or setup an icinga [satelite](https://www.icinga.com/docs/icinga2/latest/doc/06-distributed-monitoring) container which will live in kubernetes. Of course you may also deploy an entire icinga2 setup on kubernetes if you do not have an existing one. +Ingresses, NodePort and Load Balancer services will expose your kubernetes services to the public but not ClusterIP services. ClusterIP services (Which is the default) are only internal kubernetes services and not available from outside the cluster. If your icinga2 setup lives outside the cluster you only have two options, either disable deployment for those objects or setup an icinga [satelite](https://www.icinga.com/docs/icinga2/latest/doc/06-distributed-monitoring) container which will live on kubernetes. Of course you may also deploy an entire icinga2 setup on kubernetes if you do not have an existing one. ### Using icinga2 apply rules -You certainly can use icinga2 apply rules. You may disable auto service deployments via `applyServices` for ingresses and services and define your own services via [apply rules](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#using-apply). +You certainly can use icinga2 apply rules. You may disable auto service deployments via `applyServices` for ingresses, services and volumes and define your own services via [apply rules](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#using-apply). Of course you can also setup a mixed deployment. Automatically let kube-icinga deploy services and apply additional services via apply rules. >**Note**: Since icinga apply rules are [not triggered](https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/#modifying-objects) if an object gets updated kube-icinga will delete and recreate those objects. -All icinga host object are created with all kubernetes data available packed in the variable `vars.kubernetes`. Therefore you can apply rules with this data, for example this will create an icinga service for all objects with a kubernetes label foo=bar. +All icinga host objects are created with all kubernetes data available packed in the variable `vars.kubernetes`. Therefore you may apply rules with this data. + +For example kube-icinga will create a icinga host object for a kubernetes service: +``` + +``` + +You may use this data to dynamically create icinga apply rules: +``` +``` + ### Globally overwrite icinga object definitions It is possible to set custom options for the icinga objects during creation. You might set custom values via `kubernetes.ingresses.hostTemplates` or kubernetes.ingresses.serviceTemplate`. The same can also be done for services. For example it might be crucial to set a custom zone for ClusterIP services since they are only reachable within the cluster and shall be monitored by an icinga satelite node. Any valid setting for a specific icinga object can be set. @@ -101,9 +124,13 @@ you may configure custom incinga attributes directly in the kubernetes resource ### Overwrite icinga object definitions directly in kubernetes resources kube-icinga is able to parse kubernetes annotations and merge those with its default settings. + +>**Note**: With annotations you may set specific settings for each kubernetes resources while configure options for kube-icinga set settings on a resource type basis. + You may use the following annotations: | Name | Description | +|-------|------------| | `kube-icinga/check_command` | Use a custom icinga check command. | | `kube-icinga/template` | Use a custom icinga template. | | `kube-icinga/definition` | JSON encoded icinga definiton which may contain advanced icinga options and gets merged with the defaults. | diff --git a/config.json b/config.json index e0a4db8..e8bf34e 100644 --- a/config.json +++ b/config.json @@ -34,7 +34,7 @@ }, "services": { "ClusterIP": { - "discover": false, + "discover": true, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b0b30e1..08e59ac 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -6,9 +6,8 @@ services: # Set your hostname to the FQDN under which your # sattelites will reach this container hostname: icinga2 - env_file: - - secrets_sql.env environment: + - MYSQL_ROOT_PASSWORD= - ICINGA2_FEATURE_GRAPHITE=1 # Important: # keep the hostname graphite the same as @@ -55,8 +54,8 @@ services: - ./data/graphite/log/carbon:/var/log/carbon mysql: image: mariadb:10.1 - env_file: - - secrets_sql.env + environment: + - MYSQL_ROOT_PASSWORD= volumes: - ./data/mysql/data:/var/lib/mysql # If you have previously used the container's internal DB use: diff --git a/src/icinga.ts b/src/icinga.ts index 7175a4f..dbff9fc 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -177,11 +177,12 @@ export default class Icinga { this.logger.info(`delete service ${name} from host ${host}`); return this.icingaClient.deleteService(name, host, (err, result) => { - resolve(true); if (err) { this.logger.error(`failed delete service ${name} from host ${host}`, {error: err}); + resolve(false); } else { this.logger.info(`service ${name} was deleted successfully from host ${host}`, {result: result}); + resolve(true); } }); }); @@ -195,11 +196,12 @@ export default class Icinga { this.logger.info(`delete host ${name}`); this.icingaClient.deleteHost(name, (err, result) => { - resolve(true); if (err) { this.logger.error(`failed delete host ${name}`, {error: err}); + resolve(false); } else { this.logger.info(`host ${name} was deleted successfully`, {result: result}); + resolve(true); } }); }); diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index 0486c8a..fb90f58 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -4,12 +4,21 @@ import JSONStream from 'json-stream'; import KubeNode from './node'; import Resource from './abstract.resource'; +interface IngressOptions { + discover?: boolean; + applyServices?: boolean; + attachToNodes?: boolean; + hostDefinition?: object; + serviceDefinition?: object; + hostTemplates?: string[]; + serviceTemplates?: string[]; +} + /** * kubernetes ingresses */ export default class Ingress extends Resource { protected logger: Logger; - protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; protected kubeNode: KubeNode; @@ -25,10 +34,9 @@ export default class Ingress extends Resource { /** * kubernetes hosts */ - constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, jsonStream: JSONStream, options: IngressOptions) { super(); this.logger = logger; - this.kubeClient = kubeClient; this.icinga = icinga; this.jsonStream = jsonStream; this.kubeNode = kubeNode; @@ -71,8 +79,9 @@ export default class Ingress extends Resource { * Preapre icinga object and apply */ public async prepareObject(definition: any): Promise { + let hostname = this.escapeName(['ingress', definition.metadata.namespace, definition.metadata.name].join('-')); if (!this.options.attachToNodes) { - await this.applyHost(definition.metadata.name, definition.metadata.name, definition, this.options.hostTemplates); + await this.applyHost(hostname, definition.metadata.name, definition, this.options.hostTemplates); } let service = this.prepareResource(definition); @@ -85,9 +94,10 @@ export default class Ingress extends Resource { for (const spec of definition.spec.rules) { for (const path of spec.http.paths) { let base = path.path || '/'; + let name = this.escapeName([spec.host, 'http', base].join('-')); let addition = { 'check_command': 'http', - 'display_name': `${spec.host}:http`, + 'display_name': `${spec.host}${base}:http`, 'vars._kubernetes': true, 'vars.kubernetes': definition, 'vars.http_address': spec.host, @@ -99,13 +109,14 @@ export default class Ingress extends Resource { Object.assign(addition, this.options.serviceDefinition); Object.assign(addition, service); - this.applyService(definition.metadata.name, addition.display_name, addition, templates); + this.applyService(hostname, name, addition, templates); // tls secret set, also apply https service if (definition.spec.tls) { + name = this.escapeName([spec.host, 'https', base].join('-')); addition.display_name += 's'; addition['vars.http_ssl'] = true; - this.applyService(definition.metadata.name, addition.display_name, addition, templates); + this.applyService(hostname, name, addition, templates); } } } @@ -115,9 +126,9 @@ export default class Ingress extends Resource { /** * Start kube listener */ - public async kubeListener(): Promise { + public async kubeListener(provider) { try { - const stream = this.kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); + var stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { this.logger.debug('received kubernetes ingress resource', {object}); @@ -139,7 +150,7 @@ export default class Ingress extends Resource { }); this.jsonStream.on('finish', () => { - this.kubeListener(); + this.kubeListener(provider); }); } catch (err) { this.logger.error('failed start ingresses listener', {error: err}); diff --git a/src/kube/node.ts b/src/kube/node.ts index a7d9167..cc14eae 100644 --- a/src/kube/node.ts +++ b/src/kube/node.ts @@ -3,17 +3,22 @@ import Icinga from '../icinga'; import JSONStream from 'json-stream'; import Resource from './abstract.resource'; +interface NodeOptions { + discover?: boolean; + hostDefinition?: object; + hostTemplates?: string[]; +} + /** * kubernetes hosts */ export default class Node extends Resource { protected logger: Logger; - protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; protected nodes: string[] = []; - protected options = { - discovery: true, + protected options: NodeOptions = { + discover: true, hostDefinition: {}, hostTemplates: [], }; @@ -21,10 +26,9 @@ export default class Node extends Resource { /** * kubernetes hosts */ - constructor(logger: Logger, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + constructor(logger: Logger, icinga: Icinga, jsonStream: JSONStream, options: NodeOptions) { super(); this.logger = logger; - this.kubeClient = kubeClient; this.icinga = icinga; this.jsonStream = jsonStream; this.options = Object.assign(this.options, options); @@ -33,22 +37,22 @@ export default class Node extends Resource { /** * Preapre icinga object and apply */ - protected async prepareObject(definition: any): Promise { + public async prepareObject(definition: any): Promise { let host = { 'display_name': definition.metadata.name, - 'host_name': definition.metadata.name, + 'address': definition.metadata.name, 'vars._kubernetes': true, 'vars.kubernetes': definition, - 'groups': [definition.metadata.namespace], + 'check_command': 'ping', }; if (!definition.spec.unschedulable) { this.logger.debug('skip kube worker node '+definition.metadata.name+' since it is flagged as unschedulable'); this.nodes.push(definition.metadata.name); } - - host = Object.assign(host, this.options.hostDefinition); - return this.icinga.applyHost(host.host_name, host, this.options.hostTemplates); + + Object.assign(host, this.options.hostDefinition); + return this.icinga.applyHost(definition.metadata.name, host, this.options.hostTemplates); } /** @@ -61,9 +65,9 @@ export default class Node extends Resource { /** * Start kube listener */ - public async kubeListener(): Promise { + public async kubeListener(provider) { try { - const stream = this.kubeClient.apis.v1.watch.nodes.getStream(); + const stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { // ignore MODIFIER for kube nodes @@ -89,7 +93,7 @@ export default class Node extends Resource { }); this.jsonStream.on('finish', () => { - this.kubeListener(); + this.kubeListener(provider); }); } catch (err) { this.logger.error('failed start nodes listener', {error: err}); diff --git a/src/kube/service.ts b/src/kube/service.ts index 48d4fec..13a7a51 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -55,7 +55,6 @@ export default class Service extends Resource { static readonly TYPE_LOADBALANCER = 'LoadBalancer'; protected logger: Logger; - protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; protected kubeNode: KubeNode; @@ -64,10 +63,9 @@ export default class Service extends Resource { /** * kubernetes services */ - constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options: ServiceOptions=defaults) { + constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, jsonStream: JSONStream, options: ServiceOptions=defaults) { super(); this.logger = logger; - this.kubeClient = kubeClient; this.icinga = icinga; this.jsonStream = jsonStream; var clone = JSON.parse(JSON.stringify(defaults)); @@ -101,7 +99,6 @@ export default class Service extends Resource { protected async applyService(host: string, name: string, type: string, definition, templates: string[]) { if (type === Service.TYPE_NODEPORT) { for (const node of this.kubeNode.getWorkerNodes()) { - definition.host_name = node; this.icinga.applyService(node, name, definition, templates); } } else { @@ -121,14 +118,15 @@ export default class Service extends Resource { } var options = this.options[serviceType]; - var service = options.serviceDefinition; + var service = JSON.parse(JSON.stringify(options.serviceDefinition)); service['groups'] = [definition.metadata.namespace]; Object.assign(service, this.prepareResource(definition)); + let hostname = this.escapeName(['service', definition.metadata.namespace, definition.metadata.name].join('-')); var templates = options.serviceTemplates; templates = templates.concat(this.prepareTemplates(definition)); if (serviceType !== Service.TYPE_NODEPORT) { - await this.applyHost(definition.metadata.name, definition.spec.clusterIP, serviceType, definition, options.hostTemplates); + await this.applyHost(hostname, definition.spec.clusterIP, serviceType, definition, options.hostTemplates); } if (options.applyServices) { @@ -140,26 +138,27 @@ export default class Service extends Resource { let hasCommand = await this.icinga.hasCheckCommand(port.check_command); if (hasCommand) { this.logger.debug('service can be checked via check command '+port.check_command); - port.display_name = name; port['vars.'+port.check_command+'_port'] = servicePort.nodePort || servicePort.port; } else { delete port.check_command; this.logger.warn('service can not be checked via check command '+port.check_command+', icinga check command does not exists, fallback to service protocol '+servicePort.protocol); } } + + let protocol = servicePort.protocol.toLowerCase(); + let port_name = servicePort.name || protocol+':'+servicePort.port; if (!port.check_command) { - let protocol = servicePort.protocol.toLowerCase(); - let name = servicePort.name || protocol+':'+servicePort.port; port.check_command = protocol; - port.display_name = name.toLowerCase(); port['vars.'+protocol+'_port'] = servicePort.nodePort || servicePort.port; } port['vars._kubernetes'] = true; port['vars.kubernetes'] = definition; + let name = this.escapeName([definition.metadata.name, port_name].join('-')); + port['display_name'] = name; - this.applyService(definition.metadata.name, port.display_name, serviceType, port, templates); + this.applyService(hostname, name, serviceType, port, templates); } } } @@ -167,9 +166,9 @@ export default class Service extends Resource { /** * Start kube listener */ - public async kubeListener(): Promise { + public async kubeListener(provider) { try { - const stream = this.kubeClient.apis.v1.watch.services.getStream(); + var stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { this.logger.debug('received kubernetes service resource', {object}); @@ -196,7 +195,7 @@ export default class Service extends Resource { }); this.jsonStream.on('finish', () => { - this.kubeListener(); + this.kubeListener(provider); }); } catch (err) { this.logger.error('failed start services listener', {error: err}); diff --git a/src/kube/volume.ts b/src/kube/volume.ts index 01c675c..3428011 100644 --- a/src/kube/volume.ts +++ b/src/kube/volume.ts @@ -4,12 +4,21 @@ import JSONStream from 'json-stream'; import KubeNode from './node'; import Resource from './abstract.resource'; +interface VolumeOptions { + discover?: boolean; + applyServices?: boolean; + attachToNodes?: boolean; + hostDefinition?: object; + serviceDefinition?: object; + hostTemplates?: string[]; + serviceTemplates?: string[]; +} + /** * kubernetes ingresses */ export default class Volume extends Resource { protected logger: Logger; - protected kubeClient; protected icinga: Icinga; protected jsonStream: JSONStream; protected kubeNode: KubeNode; @@ -25,10 +34,9 @@ export default class Volume extends Resource { /** * kubernetes hosts */ - constructor(logger: Logger, kubeNode: KubeNode, kubeClient, icinga: Icinga, jsonStream: JSONStream, options) { + constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, jsonStream: JSONStream, options: VolumeOptions) { super(); this.logger = logger; - this.kubeClient = kubeClient; this.icinga = icinga; this.jsonStream = jsonStream; this.kubeNode = kubeNode; @@ -71,8 +79,8 @@ export default class Volume extends Resource { * Preapre icinga object and apply */ public async prepareObject(definition: any): Promise { - var host = this.escapeName(definition.metadata.annotations['pv.kubernetes.io/provisioned-by']); - await this.applyHost(host, host, definition, this.options.hostTemplates); + var hostname = this.escapeName(definition.metadata.annotations['pv.kubernetes.io/provisioned-by']); + await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); if (this.options.applyServices) { var groups = []; @@ -85,7 +93,7 @@ export default class Volume extends Resource { templates = templates.concat(this.prepareTemplates(definition)); let service = this.options.serviceDefinition; - let name = this.escapeName(definition.metadata.name); + let name = this.escapeName(['volume', definition.metadata.name].join('-')); let addition = { 'check_command': 'dummy', 'display_name': `${definition.metadata.name}:volume`, @@ -96,16 +104,16 @@ export default class Volume extends Resource { Object.assign(addition, service); Object.assign(addition, this.prepareResource(definition)); - this.applyService(host, name, addition, templates); + this.applyService(hostname, name, addition, templates); } } /** * Start kube listener */ - public async kubeListener(): Promise { + public async kubeListener(provider) { try { - const stream = this.kubeClient.apis.v1.watch.persistentvolumes.getStream(); + var stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { this.logger.debug('received kubernetes persistent volume resource', {object}); @@ -127,7 +135,7 @@ export default class Volume extends Resource { }); this.jsonStream.on('finish', () => { - this.kubeListener(); + this.kubeListener(provider); }); } catch (err) { this.logger.error('failed start ingresses listener', {error: err}); diff --git a/src/main.ts b/src/main.ts index 4f3c16f..47d6efd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,10 +10,10 @@ import Volume from './kube/volume'; import * as JSONStream from 'json-stream'; const icinga = new IcingaWrapper(logger, icingaClient); -const kubeNode = new Node(logger, kubeClient, icinga, new JSONStream(), config.kubernetes.nodes); -const kubeIngress = new Ingress(logger, kubeNode, kubeClient, icinga, new JSONStream(), config.kubernetes.ingresses); -const kubeService = new Service(logger, kubeNode, kubeClient, icinga, new JSONStream(), config.kubernetes.services); -const kubeVolume = new Volume(logger, kubeNode, kubeClient, icinga, new JSONStream(), config.kubernetes.volumes); +const kubeNode = new Node(logger, icinga, new JSONStream(), config.kubernetes.nodes); +const kubeIngress = new Ingress(logger, kubeNode, icinga, new JSONStream(), config.kubernetes.ingresses); +const kubeService = new Service(logger, kubeNode, icinga, new JSONStream(), config.kubernetes.services); +const kubeVolume = new Volume(logger, kubeNode, icinga, new JSONStream(), config.kubernetes.volumes); /** * Main @@ -26,21 +26,29 @@ async function main() { } if (config.kubernetes.nodes.discover) { - // kubeNode.kubeListener(); + kubeNode.kubeListener(() => { + return kubeClient.apis.v1.watch.nodes.getStream(); + }); } - + if (config.kubernetes.ingresses.discover) { - // kubeIngress.kubeListener(); + kubeIngress.kubeListener(() => { + return kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); + }); } if (config.kubernetes.volumes.discover) { - kubeVolume.kubeListener(); + kubeVolume.kubeListener(() => { + return kubeClient.apis.v1.watch.persistentvolumes.getStream(); + }); } if (config.kubernetes.services.ClusterIP.discover || config.kubernetes.services.NodePort.discover || config.kubernetes.services.LoadBalancer.discover) { - // kubeService.kubeListener(); + kubeService.kubeListener(() => { + return kubeClient.apis.v1.watch.services.getStream(); + }); } } diff --git a/tests/icinga.test.ts b/tests/icinga.test.ts index ffed4c0..c170485 100644 --- a/tests/icinga.test.ts +++ b/tests/icinga.test.ts @@ -9,10 +9,10 @@ public async cleanup(): Promise { */ import * as IcingaApi from 'icinga2-api'; -import IcingaClient from '../../src/icinga'; -import Logger from '../../src/logger'; +import IcingaClient from '../src/icinga'; +import Logger from '../src/logger'; jest.mock('icinga2-api'); -jest.mock('../../src/logger'); +jest.mock('../src/logger'); var icinga; beforeEach(() => { @@ -330,4 +330,50 @@ describe('icinga', () => { expect(calls[0][2]).toBe('bar'); }); }); + + describe('delete service', () => { + it('delete service successfully', async () => { + IcingaApi.deleteService = jest.fn() + .mockImplementation((host, service, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.deleteService('foo', 'bar')).resolves.toEqual(true); + const calls = IcingaApi.deleteService.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('bar'); + expect(calls[0][1]).toBe('foo'); + }); + + it('delete service failed', async () => { + IcingaApi.deleteService = jest.fn() + .mockImplementation((host, service, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.deleteService('foo', 'bar')).resolves.toEqual(false); + const calls = IcingaApi.deleteService.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('bar'); + expect(calls[0][1]).toBe('foo'); + }); + }); + + describe('delete host', () => { + it('delete host successfully', async () => { + IcingaApi.deleteHost = jest.fn() + .mockImplementation((host, cb) => cb(null, {foo: "bar"})); + + await expect(icinga.deleteHost('bar')).resolves.toEqual(true); + const calls = IcingaApi.deleteHost.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('bar'); + }); + + it('delete host failed', async () => { + IcingaApi.deleteHost = jest.fn() + .mockImplementation((host, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.deleteHost('bar')).resolves.toEqual(false); + const calls = IcingaApi.deleteHost.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('bar'); + }); + }); }); diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index 9de336e..8d575c8 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -3,9 +3,7 @@ import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; import {LoggerInstance} from 'winston'; import * as JSONStream from 'json-stream'; -const KubeApi = require('kubernetes-client').Client; jest.mock('../../src/icinga'); -jest.mock('../../src/kube/node'); jest.mock('json-stream'); jest.mock('kubernetes-client'); @@ -64,17 +62,20 @@ describe('kubernetes ingresses', () => { describe('add ingress object with dummy host', () => { it('create icinga host object', () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga); + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false + }); Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; - expect(call[0]).toBe('foo'); - expect(call[1].display_name).toBe('foo'); + expect(call[0]).toBe('ingress-foobar-foo'); + expect(call[1].display_name).toBe('ingress-foobar-foo'); expect(call[1].check_command).toBe('dummy'); }); it('create icinga host object with custom definitions', () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, hostDefinition: { 'vars.foo': 'bar', 'vars.check_command': 'foo' @@ -89,7 +90,8 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with templates', () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, hostTemplates: ['foo', 'bar'] }); @@ -100,7 +102,8 @@ describe('kubernetes ingresses', () => { }); it('do not create icinga host object while attachToNodes is enabled', () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, attachToNodes: true }); @@ -112,7 +115,7 @@ describe('kubernetes ingresses', () => { describe('add ingress object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream); + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream); Icinga.applyHost = jest.fn(); Icinga.applyService = jest.fn(); @@ -123,7 +126,7 @@ describe('kubernetes ingresses', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: false }); @@ -136,22 +139,23 @@ describe('kubernetes ingresses', () => { describe('add all ingress object http path rules as service objects', () => { it('create all service objects', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream); + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream); + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; expect(Icinga.applyService.mock.instances.length).toBe(2); - expect(calls[0][0]).toBe('foo'); - expect(calls[1][0]).toBe('foo'); - expect(calls[0][1]).toBe('foobar.example.org:http'); - expect(calls[1][1]).toBe('barfoo.example.org:http'); + expect(calls[0][0]).toBe('ingress-foobar-foo'); + expect(calls[1][0]).toBe('ingress-foobar-foo'); + expect(calls[0][1]).toBe('foobar.example.org-http--'); + expect(calls[1][1]).toBe('barfoo.example.org-http--foo'); expect(calls[0][2]['vars.http_path']).toBe('/'); expect(calls[1][2]['vars.http_path']).toBe('/foo'); }); it('create all service objects with custom service definition', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: true, serviceDefinition: { 'check_command': 'tcp', @@ -170,7 +174,7 @@ describe('kubernetes ingresses', () => { }); it('create all service objects with templates', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -184,7 +188,7 @@ describe('kubernetes ingresses', () => { }); it('create service objects for tls enabled ingresses', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -197,8 +201,8 @@ describe('kubernetes ingresses', () => { await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; expect(Icinga.applyService.mock.instances.length).toBe(4); - expect(calls[0][1]).toBe('foobar.example.org:http'); - expect(calls[1][1]).toBe('foobar.example.org:https'); + expect(calls[0][1]).toBe('foobar.example.org-http--'); + expect(calls[1][1]).toBe('foobar.example.org-https--'); expect(calls[1][2]['vars.http_ssl']).toBe(true); }); @@ -206,13 +210,14 @@ describe('kubernetes ingresses', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: true }); fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; fixture.metadata.annotations['kube-icinga/templates'] = 'foobar,barfoo'; + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; @@ -224,7 +229,7 @@ describe('kubernetes ingresses', () => { }); it('use annotation instead global definition', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: true, serviceDefinition: { check_command: 'foo' @@ -232,6 +237,7 @@ describe('kubernetes ingresses', () => { }); fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; @@ -241,7 +247,7 @@ describe('kubernetes ingresses', () => { }); it('definiton merge', async () => { - let instance = new Ingress(LoggerInstance, Node, KubeApi, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { applyServices: true, serviceDefinition: { check_command: 'foo' @@ -249,6 +255,7 @@ describe('kubernetes ingresses', () => { }); fixture.metadata.annotations['kube-icinga/definition'] = '{"vars.foo": "foobar"}'; + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; diff --git a/tests/kube/node.test.ts b/tests/kube/node.test.ts new file mode 100644 index 0000000..85b9ebd --- /dev/null +++ b/tests/kube/node.test.ts @@ -0,0 +1,98 @@ +import Node from '../../src/kube/node'; +import Icinga from '../../src/icinga'; +import Logger from '../../src/logger'; +import * as JSONStream from 'json-stream'; +jest.mock('../../src/icinga'); +jest.mock('../../src/logger'); +const Readable = require('stream').Readable; + +const template = { + "apiVersion": "v1", + "kind": "Node", + "metadata": { + "annotations": {}, + "name": "kubernetes-worker001.foo.bar", + }, + "spec": { + "externalID": "kubernetes-worker001.foo.bar" + } +}; + +var fixture; + +beforeEach(() => { + fixture = JSON.parse(JSON.stringify(template)); +}); + +describe('kubernetes nodes', () => { + describe('nodes watch stream', () => { + it('create icinga host object', () => { + let instance = new Node(Logger, Icinga, new JSONStream()); + instance.prepareObject = jest.fn(); + /*Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0];*/ + var stream = new Readable(); + stream._read = () => {}; + stream.push(JSON.stringify([{ + type: 'MODIFIED', + object: fixture + }])); + + instance.kubeListener(() => { + return stream; + }); + }); + }); + + describe('add node object', () => { + it('create icinga host object', () => { + let instance = new Node(Logger, Icinga, JSONStream); + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('kubernetes-worker001.foo.bar'); + expect(call[1].display_name).toBe('kubernetes-worker001.foo.bar'); + expect(call[1].check_command).toBe('ping'); + expect(instance.getWorkerNodes()).toEqual(['kubernetes-worker001.foo.bar']); + }); + + it('create icinga host object (kubernetes worker unschedulable)', () => { + let instance = new Node(Logger, Icinga, JSONStream); + Icinga.applyHost = jest.fn(); + fixture.spec.unschedulable = true; + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('kubernetes-worker001.foo.bar'); + expect(call[1].display_name).toBe('kubernetes-worker001.foo.bar'); + expect(instance.getWorkerNodes()).toEqual([]); + }); + + it('create icinga host object with custom definitions', () => { + let instance = new Node(Logger, Icinga, JSONStream, { + hostDefinition: { + 'vars.foo': 'bar', + 'vars.check_command': 'foo' + } + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[1]['vars.foo']).toBe('bar'); + expect(call[1]['vars.check_command']).toBe('foo'); + }); + + it('create icinga host object with templates', () => { + let instance = new Node(Logger, Icinga, JSONStream, { + hostTemplates: ['foo', 'bar'] + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[2]).toEqual(['foo', 'bar']); + }); + }); +}); + diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts index f5f583d..90d596c 100644 --- a/tests/kube/service.test.ts +++ b/tests/kube/service.test.ts @@ -3,11 +3,9 @@ import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; import Logger from '../../src/logger'; import * as JSONStream from 'json-stream'; -const KubeApi = require('kubernetes-client').Client; jest.mock('../../src/icinga'); jest.mock('../../src/kube/node'); jest.mock('json-stream'); -jest.mock('kubernetes-client'); jest.mock('../../src/logger'); const template = { @@ -52,24 +50,27 @@ beforeEach(() => { }); describe('kubernetes services', () => { - var instance: Service; - describe('add service object with dummy host', () => { it('create icinga host object', () => { - let instance = new Service(Logger, Node, KubeApi, Icinga); + let instance = new Service(Logger, Node, Icinga, { + ClusterIP: { + applyServices: false + } + }); + Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; - expect(call[0]).toBe('foo'); + expect(call[0]).toBe('service-foobar-foo'); expect(call[1].address).toBe('10.99.24.32'); - expect(call[1].display_name).toBe('foo'); + expect(call[1].display_name).toBe('service-foobar-foo'); expect(call[1].check_command).toBe('dummy'); }); it('create icinga host object with custom definitions', () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { - applyServices: true, + applyServices: false, hostDefinition: { 'vars.foo': 'bar', 'vars.check_command': 'foo' @@ -77,21 +78,23 @@ describe('kubernetes services', () => { } }); + Icinga.applyServiceGroup = jest.fn(); Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); - console.log(Icinga.applyHost.mock.calls); const call = Icinga.applyHost.mock.calls[0]; expect(call[1]['vars.foo']).toBe('bar'); expect(call[1]['vars.check_command']).toBe('foo'); }); it('create icinga host object with templates', () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { + applyServices: false, hostTemplates: ['foo', 'bar'] } }); + Icinga.applyServiceGroup = jest.fn(); Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; @@ -99,7 +102,11 @@ describe('kubernetes services', () => { }); it('do not create icinga host object while service is of type NodePort', () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream); + let instance = new Service(Logger, Node, Icinga, JSONStream, { + NodePort: { + applyServices: false + } + }); fixture.spec.type = 'NodePort'; Icinga.applyHost = jest.fn(); @@ -110,7 +117,7 @@ describe('kubernetes services', () => { describe('add service object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream); + let instance = new Service(Logger, Node, Icinga, JSONStream); Icinga.applyHost = jest.fn(); Icinga.hasCheckCommand = jest.fn(); @@ -122,12 +129,12 @@ describe('kubernetes services', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { applyServices: false }); - Icinga.applyServiceGroup = jest.fn(); instance.prepareObject(fixture); + Icinga.applyServiceGroup = jest.fn(); const call = Icinga.applyServiceGroup.mock.instances[0]; expect(Icinga.applyServiceGroup.mock.instances.length).toBe(0); }); @@ -135,17 +142,17 @@ describe('kubernetes services', () => { describe('add all service object ports as service objects', () => { it('create all service objects', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream); + let instance = new Service(Logger, Node, Icinga, JSONStream); Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; - console.log(calls); expect(Icinga.applyService.mock.instances.length).toBe(2); - expect(calls[0][0]).toBe('foo'); - expect(calls[1][0]).toBe('foo'); - expect(calls[0][1]).toBe('http'); - expect(calls[1][1]).toBe('bar'); + expect(calls[0][0]).toBe('service-foobar-foo'); + expect(calls[1][0]).toBe('service-foobar-foo'); + expect(calls[0][1]).toBe('foo-http'); + expect(calls[1][1]).toBe('foo-bar'); expect(calls[0][2]['check_command']).toBe('tcp'); expect(calls[0][2]['vars.tcp_port']).toBe(80); @@ -154,7 +161,7 @@ describe('kubernetes services', () => { }); it('create all service objects with custom service definition', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -180,7 +187,7 @@ describe('kubernetes services', () => { }); it('create all service objects with custom service definition, check_command not found, fallback to protocol', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -206,13 +213,14 @@ describe('kubernetes services', () => { }); it('create all service objects with templates', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { applyServices: true, serviceTemplates: ['foo', 'bar'] } }); + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; @@ -224,7 +232,7 @@ describe('kubernetes services', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { applyServices: true } @@ -248,7 +256,7 @@ describe('kubernetes services', () => { }); it('use annotation instead global definition', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -258,6 +266,7 @@ describe('kubernetes services', () => { }); fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); Icinga.hasCheckCommand = jest.fn(); Icinga.hasCheckCommand.mockResolvedValue(true); @@ -270,7 +279,7 @@ describe('kubernetes services', () => { }); it('definiton merge', async () => { - let instance = new Service(Logger, Node, KubeApi, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -280,6 +289,7 @@ describe('kubernetes services', () => { }); fixture.metadata.annotations['kube-icinga/definition'] = '{"vars.foo": "foobar"}'; + Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); Icinga.hasCheckCommand = jest.fn(); Icinga.hasCheckCommand.mockResolvedValue(true); From 8dcfb39666fb00df0d57cbf274ebe9742e67ed76 Mon Sep 17 00:00:00 2001 From: raffis Date: Thu, 14 Feb 2019 09:40:52 +0100 Subject: [PATCH 07/15] fixes --- config.json | 2 +- src/client/kube.ts | 6 +-- src/icinga.ts | 20 +++++----- src/kube/abstract.resource.ts | 32 ++++++++-------- src/kube/ingress.ts | 16 ++++---- src/kube/node.ts | 8 ++-- src/kube/service.ts | 72 +++++++++++++++++------------------ src/kube/volume.ts | 20 +++++----- src/logger.ts | 8 ++-- src/main.ts | 10 ++--- tests/icinga.test.ts | 16 ++------ 11 files changed, 101 insertions(+), 109 deletions(-) diff --git a/config.json b/config.json index e8bf34e..e0a4db8 100644 --- a/config.json +++ b/config.json @@ -34,7 +34,7 @@ }, "services": { "ClusterIP": { - "discover": true, + "discover": false, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], diff --git a/src/client/kube.ts b/src/client/kube.ts index 10382b1..61947ad 100644 --- a/src/client/kube.ts +++ b/src/client/kube.ts @@ -1,12 +1,12 @@ const KubeApi = require('kubernetes-client').Client; const KubeConfig = require('kubernetes-client').config; -var config; -if(process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT) { +let config; +if (process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT) { config = KubeConfig.getInCluster(); } else { config = KubeConfig.fromKubeconfig(); } - + const kubeClient = new KubeApi({config: config, version: '1.9'}); export default kubeClient; diff --git a/src/icinga.ts b/src/icinga.ts index dbff9fc..c698531 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -50,10 +50,10 @@ export default class Icinga { this.icingaClient.createHostGroup(name, name, [], (err, result) => { if (err) { this.logger.error(`failed create host group ${name}`, {error: err}); - reject(err); + resolve(false); } else { this.logger.info(`host group ${name} was created successfully`, {result: result}); - resolve(true); + resolve(true); } }); } else { @@ -81,10 +81,10 @@ export default class Icinga { this.icingaClient.createServiceGroup(name, name, [], (err, result) => { if (err) { this.logger.error(`failed create service group ${name}`, {error: err}); - reject(err); + resolve(false); } else { this.logger.info(`service group ${name} was created successfully`, {result: result}); - resolve(true); + resolve(true); } }); } else { @@ -117,7 +117,7 @@ export default class Icinga { this.icingaClient.createHostCustom(JSON.stringify(host), name, (err, result) => { if (err) { this.logger.error(`failed create host ${name}`, {error: err}); - reject(err); + resolve(false); } else { this.logger.info(`host ${name} was created successfully`, {result: result}); resolve(true); @@ -153,7 +153,7 @@ export default class Icinga { this.icingaClient.createServiceCustom(JSON.stringify(service), host, name, (err, result) => { if (err) { this.logger.error(`failed create service ${name} on host ${host}`, {error: err}); - reject(err); + resolve(false); } else { this.logger.info(`service ${name} on host ${host} was created successfully`, {result: result}); resolve(true); @@ -215,11 +215,11 @@ export default class Icinga { return new Promise((resolve, reject) => { this.icingaClient.getServiceFiltered({filter: 'service.vars._kubernetes == true'}, async (err, result) => { - if(err) { + if (err) { return reject(err); } - var handlers = []; + let handlers = []; for (const service of result) { handlers.push(this.deleteService(service.attrs.host_name, service.attrs.name)); } @@ -229,11 +229,11 @@ export default class Icinga { }); this.icingaClient.getHostFiltered({filter: 'host.vars._kubernetes == true'}, async (err, result) => { - if(err) { + if (err) { return reject(err); } - var handlers = []; + let handlers = []; for (const host of result) { handlers.push(this.deleteHost(host.attrs.name)); } diff --git a/src/kube/abstract.resource.ts b/src/kube/abstract.resource.ts index 5bf78ef..62d4d6c 100644 --- a/src/kube/abstract.resource.ts +++ b/src/kube/abstract.resource.ts @@ -17,22 +17,22 @@ export default abstract class Resource { * Prepare icinga object with kube annotations */ protected prepareResource(resource: any): any { - var definition: any = {}; + let definition: any = {}; - if(!resource.metadata.annotations) { - return definition; + if (!resource.metadata.annotations) { + return definition; } - var annotations: any = resource.metadata.annotations; + let annotations: any = resource.metadata.annotations; - if(annotations['kube-icinga/check_command']) { - definition.check_command = annotations['kube-icinga/check_command']; + if (annotations['kube-icinga/check_command']) { + definition.check_command = annotations['kube-icinga/check_command']; } - - if(annotations['kube-icinga/definition']) { - Object.assign(definition, JSON.parse(annotations['kube-icinga/definition'])); + + if (annotations['kube-icinga/definition']) { + Object.assign(definition, JSON.parse(annotations['kube-icinga/definition'])); } - + return definition; } @@ -40,14 +40,14 @@ export default abstract class Resource { * Prepare icinga object with kube annotations */ protected prepareTemplates(resource: any): string[] { - if(!resource.metadata.annotations) { - return []; + if (!resource.metadata.annotations) { + return []; } - var annotations: any = resource.metadata.annotations; - - if(annotations['kube-icinga/templates']) { - return annotations['kube-icinga/templates'].split(','); + let annotations: any = resource.metadata.annotations; + + if (annotations['kube-icinga/templates']) { + return annotations['kube-icinga/templates'].split(','); } return []; diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index fb90f58..0ee8df6 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -41,7 +41,7 @@ export default class Ingress extends Resource { this.jsonStream = jsonStream; this.kubeNode = kubeNode; this.options = Object.assign(this.options, options); - } + } /** * Apply host @@ -85,7 +85,7 @@ export default class Ingress extends Resource { } let service = this.prepareResource(definition); - var templates = this.options.serviceTemplates; + let templates = this.options.serviceTemplates; templates = templates.concat(this.prepareTemplates(definition)); if (this.options.applyServices) { @@ -94,7 +94,7 @@ export default class Ingress extends Resource { for (const spec of definition.spec.rules) { for (const path of spec.http.paths) { let base = path.path || '/'; - let name = this.escapeName([spec.host, 'http', base].join('-')); + let name = this.escapeName([spec.host, 'http', base].join('-')); let addition = { 'check_command': 'http', 'display_name': `${spec.host}${base}:http`, @@ -113,7 +113,7 @@ export default class Ingress extends Resource { // tls secret set, also apply https service if (definition.spec.tls) { - name = this.escapeName([spec.host, 'https', base].join('-')); + name = this.escapeName([spec.host, 'https', base].join('-')); addition.display_name += 's'; addition['vars.http_ssl'] = true; this.applyService(hostname, name, addition, templates); @@ -128,12 +128,12 @@ export default class Ingress extends Resource { */ public async kubeListener(provider) { try { - var stream = provider(); + let stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { this.logger.debug('received kubernetes ingress resource', {object}); - if(object.object.kind !== 'Ingress') { + if (object.object.kind !== 'Ingress') { this.logger.error('skip invalid ingress object', {object: object}); return; } @@ -143,8 +143,8 @@ export default class Ingress extends Resource { } if (object.type == 'ADDED' || object.type == 'MODIFIED') { - this.prepareObject(object.object).catch(err => { - this.logger.error('failed to handle resource', {error: err}) + this.prepareObject(object.object).catch((err) => { + this.logger.error('failed to handle resource', {error: err}); }); } }); diff --git a/src/kube/node.ts b/src/kube/node.ts index cc14eae..c0a7a32 100644 --- a/src/kube/node.ts +++ b/src/kube/node.ts @@ -43,14 +43,14 @@ export default class Node extends Resource { 'address': definition.metadata.name, 'vars._kubernetes': true, 'vars.kubernetes': definition, - 'check_command': 'ping', + 'check_command': 'ping', }; if (!definition.spec.unschedulable) { this.logger.debug('skip kube worker node '+definition.metadata.name+' since it is flagged as unschedulable'); this.nodes.push(definition.metadata.name); } - + Object.assign(host, this.options.hostDefinition); return this.icinga.applyHost(definition.metadata.name, host, this.options.hostTemplates); } @@ -76,7 +76,7 @@ export default class Node extends Resource { } this.logger.debug('received kubernetes host resource', {object}); - if(object.object.kind !== 'Node') { + if (object.object.kind !== 'Node') { this.logger.error('skip invalid node object', {object: object}); return; } @@ -86,7 +86,7 @@ export default class Node extends Resource { } if (object.type == 'ADDED') { - this.prepareObject(object.object).catch(err => { + this.prepareObject(object.object).catch((err) => { this.logger.error('failed to handle resource', {error: err}); }); } diff --git a/src/kube/service.ts b/src/kube/service.ts index 13a7a51..26ef4c9 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -20,30 +20,30 @@ interface ServiceOptions { } const defaults: ServiceOptions = { - ClusterIP: { - discover: false, - applyServices: true, - hostDefinition: {}, - serviceDefinition: {}, - hostTemplates: [], - serviceTemplates: [], - }, - NodePort: { - discover: true, - applyServices: true, - hostDefinition: {}, - serviceDefinition: {}, - hostTemplates: [], - serviceTemplates: [], - }, - LoadBalancer: { - discover: true, - applyServices: true, - hostDefinition: {}, - serviceDefinition: {}, - hostTemplates: [], - serviceTemplates: [], - }, + ClusterIP: { + discover: false, + applyServices: true, + hostDefinition: {}, + serviceDefinition: {}, + hostTemplates: [], + serviceTemplates: [], + }, + NodePort: { + discover: true, + applyServices: true, + hostDefinition: {}, + serviceDefinition: {}, + hostTemplates: [], + serviceTemplates: [], + }, + LoadBalancer: { + discover: true, + applyServices: true, + hostDefinition: {}, + serviceDefinition: {}, + hostTemplates: [], + serviceTemplates: [], + }, }; /** @@ -68,7 +68,7 @@ export default class Service extends Resource { this.logger = logger; this.icinga = icinga; this.jsonStream = jsonStream; - var clone = JSON.parse(JSON.stringify(defaults)); + let clone = JSON.parse(JSON.stringify(defaults)); Object.assign(clone.ClusterIP, options.ClusterIP); Object.assign(clone.NodePort, options.NodePort); Object.assign(clone.LoadBalancer, options.LoadBalancer); @@ -97,7 +97,7 @@ export default class Service extends Resource { * Apply service */ protected async applyService(host: string, name: string, type: string, definition, templates: string[]) { - if (type === Service.TYPE_NODEPORT) { + if (type === Service.TYPE_NODEPORT) { for (const node of this.kubeNode.getWorkerNodes()) { this.icinga.applyService(node, name, definition, templates); } @@ -113,17 +113,17 @@ export default class Service extends Resource { public async prepareObject(definition): Promise { let serviceType = definition.spec.type; - if(!this.options[serviceType]) { + if (!this.options[serviceType]) { throw new Error('unknown service type provided'); } - var options = this.options[serviceType]; - var service = JSON.parse(JSON.stringify(options.serviceDefinition)); + let options = this.options[serviceType]; + let service = JSON.parse(JSON.stringify(options.serviceDefinition)); service['groups'] = [definition.metadata.namespace]; Object.assign(service, this.prepareResource(definition)); let hostname = this.escapeName(['service', definition.metadata.namespace, definition.metadata.name].join('-')); - var templates = options.serviceTemplates; + let templates = options.serviceTemplates; templates = templates.concat(this.prepareTemplates(definition)); if (serviceType !== Service.TYPE_NODEPORT) { await this.applyHost(hostname, definition.spec.clusterIP, serviceType, definition, options.hostTemplates); @@ -140,11 +140,11 @@ export default class Service extends Resource { this.logger.debug('service can be checked via check command '+port.check_command); port['vars.'+port.check_command+'_port'] = servicePort.nodePort || servicePort.port; } else { - delete port.check_command; + delete port.check_command; this.logger.warn('service can not be checked via check command '+port.check_command+', icinga check command does not exists, fallback to service protocol '+servicePort.protocol); } } - + let protocol = servicePort.protocol.toLowerCase(); let port_name = servicePort.name || protocol+':'+servicePort.port; @@ -168,12 +168,12 @@ export default class Service extends Resource { */ public async kubeListener(provider) { try { - var stream = provider(); + let stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { this.logger.debug('received kubernetes service resource', {object}); - if(object.object.kind !== 'Service') { + if (object.object.kind !== 'Service') { this.logger.error('skip invalid service object', {object: object}); return; } @@ -188,8 +188,8 @@ export default class Service extends Resource { } if (object.type == 'ADDED' || object.type == 'MODIFIED') { - this.prepareObject(object.object).catch(err => { - this.logger.error('failed to handle resource', {error: err}) + this.prepareObject(object.object).catch((err) => { + this.logger.error('failed to handle resource', {error: err}); }); } }); diff --git a/src/kube/volume.ts b/src/kube/volume.ts index 3428011..e7e9e80 100644 --- a/src/kube/volume.ts +++ b/src/kube/volume.ts @@ -41,8 +41,8 @@ export default class Volume extends Resource { this.jsonStream = jsonStream; this.kubeNode = kubeNode; this.options = Object.assign(this.options, options); - } - + } + /** * Apply host */ @@ -79,13 +79,13 @@ export default class Volume extends Resource { * Preapre icinga object and apply */ public async prepareObject(definition: any): Promise { - var hostname = this.escapeName(definition.metadata.annotations['pv.kubernetes.io/provisioned-by']); + let hostname = this.escapeName(definition.metadata.annotations['pv.kubernetes.io/provisioned-by']); await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); if (this.options.applyServices) { - var groups = []; - if(definition.spec.claimRef.namespace) { - groups.push(definition.spec.claimRef.namespace) + let groups = []; + if (definition.spec.claimRef.namespace) { + groups.push(definition.spec.claimRef.namespace); await this.icinga.applyServiceGroup(definition.spec.claimRef.namespace); } @@ -113,12 +113,12 @@ export default class Volume extends Resource { */ public async kubeListener(provider) { try { - var stream = provider(); + let stream = provider(); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { this.logger.debug('received kubernetes persistent volume resource', {object}); - if(object.object.kind !== 'PersistentVolume') { + if (object.object.kind !== 'PersistentVolume') { this.logger.error('skip invalid object', {object: object}); return; } @@ -128,8 +128,8 @@ export default class Volume extends Resource { } if (object.type == 'ADDED' || object.type == 'MODIFIED') { - this.prepareObject(object.object).catch(err => { - this.logger.error('failed to handle resource', {error: err}) + this.prepareObject(object.object).catch((err) => { + this.logger.error('failed to handle resource', {error: err}); }); } }); diff --git a/src/logger.ts b/src/logger.ts index 60477aa..637c0e4 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,14 +1,14 @@ -import {createLogger, format, transports} from 'winston'; +import {createLogger, format, transports} from 'winston'; import config from './config'; const logger = createLogger({ level: config.log.level, format: format.combine( format.timestamp({ - format: 'YYYY-MM-DD HH:mm:ss' + format: 'YYYY-MM-DD HH:mm:ss', }), - format.json() - ), + format.json() + ), transports: [ new transports.Console(), ], diff --git a/src/main.ts b/src/main.ts index 47d6efd..17c27a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,23 +20,23 @@ const kubeVolume = new Volume(logger, kubeNode, icinga, new JSONStream(), config */ async function main() { if (config.cleanup) { - await icinga.cleanup().catch(err => { + await icinga.cleanup().catch((err) => { logger.error('failed to cleanup icinga objects', {error: err}); }); } - + /* if (config.kubernetes.nodes.discover) { kubeNode.kubeListener(() => { return kubeClient.apis.v1.watch.nodes.getStream(); }); } - + if (config.kubernetes.ingresses.discover) { kubeIngress.kubeListener(() => { - return kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); + return kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); }); } - + */ if (config.kubernetes.volumes.discover) { kubeVolume.kubeListener(() => { return kubeClient.apis.v1.watch.persistentvolumes.getStream(); diff --git a/tests/icinga.test.ts b/tests/icinga.test.ts index c170485..dd23e42 100644 --- a/tests/icinga.test.ts +++ b/tests/icinga.test.ts @@ -105,9 +105,7 @@ describe('icinga', () => { IcingaApi.getHostGroup = jest.fn() .mockImplementation((hostgroup, cb) => cb({Statuscode: 404}, null)); - await expect(icinga.applyHostGroup('foobar')).rejects.toEqual({ - Statuscode: 500 - }); + await expect(icinga.applyHostGroup('foobar')).resolves.toEqual(false); var calls = IcingaApi.getHostGroup.mock.calls; expect(calls.length).toBe(1); @@ -170,9 +168,7 @@ describe('icinga', () => { IcingaApi.getServiceGroup = jest.fn() .mockImplementation((ServiceGroup, cb) => cb({Statuscode: 404}, null)); - await expect(icinga.applyServiceGroup('foobar')).rejects.toEqual({ - Statuscode: 500 - }); + await expect(icinga.applyServiceGroup('foobar')).resolves.toEqual(false); var calls = IcingaApi.getServiceGroup.mock.calls; expect(calls.length).toBe(1); @@ -242,9 +238,7 @@ describe('icinga', () => { IcingaApi.getHostState = jest.fn() .mockImplementation((Host, cb) => cb({Statuscode: 404}, null)); - await expect(icinga.applyHost('foobar')).rejects.toEqual({ - Statuscode: 500 - }); + await expect(icinga.applyHost('foobar')).resolves.toEqual(false); var calls = IcingaApi.getHostState.mock.calls; expect(calls.length).toBe(1); @@ -315,9 +309,7 @@ describe('icinga', () => { IcingaApi.getService = jest.fn() .mockImplementation((host, service, cb) => cb({Statuscode: 404}, null)); - await expect(icinga.applyService('foobar', 'bar')).rejects.toEqual({ - Statuscode: 500 - }); + await expect(icinga.applyService('foobar', 'bar')).resolves.toEqual(false); var calls = IcingaApi.getService.mock.calls; expect(calls.length).toBe(1); From 1379bbe784ab938f31ff880a532d47d1852fe577 Mon Sep 17 00:00:00 2001 From: raffis Date: Thu, 14 Feb 2019 09:44:13 +0100 Subject: [PATCH 08/15] fixes --- src/kube/abstract.resource.ts | 4 ---- src/kube/service.ts | 4 ++-- src/main.ts | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/kube/abstract.resource.ts b/src/kube/abstract.resource.ts index 62d4d6c..a4cd12b 100644 --- a/src/kube/abstract.resource.ts +++ b/src/kube/abstract.resource.ts @@ -1,7 +1,3 @@ -import {Logger} from 'winston'; -import Icinga from '../icinga'; -import JSONStream from 'json-stream'; - /** * kubernetes hosts */ diff --git a/src/kube/service.ts b/src/kube/service.ts index 26ef4c9..6949953 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -146,7 +146,7 @@ export default class Service extends Resource { } let protocol = servicePort.protocol.toLowerCase(); - let port_name = servicePort.name || protocol+':'+servicePort.port; + let portName = servicePort.name || protocol+':'+servicePort.port; if (!port.check_command) { port.check_command = protocol; @@ -155,7 +155,7 @@ export default class Service extends Resource { port['vars._kubernetes'] = true; port['vars.kubernetes'] = definition; - let name = this.escapeName([definition.metadata.name, port_name].join('-')); + let name = this.escapeName([definition.metadata.name, portName].join('-')); port['display_name'] = name; this.applyService(hostname, name, serviceType, port, templates); diff --git a/src/main.ts b/src/main.ts index 17c27a7..bf027a8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,7 +24,7 @@ async function main() { logger.error('failed to cleanup icinga objects', {error: err}); }); } - /* + if (config.kubernetes.nodes.discover) { kubeNode.kubeListener(() => { return kubeClient.apis.v1.watch.nodes.getStream(); @@ -36,7 +36,7 @@ async function main() { return kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); }); } - */ + if (config.kubernetes.volumes.discover) { kubeVolume.kubeListener(() => { return kubeClient.apis.v1.watch.persistentvolumes.getStream(); From f02db50b6d9f5dc3ec9df306f68474d53c74b5fb Mon Sep 17 00:00:00 2001 From: raffis Date: Thu, 14 Feb 2019 14:45:05 +0100 Subject: [PATCH 09/15] fixes v2.0.0 --- CHANGELOG.md | 5 +- Dockerfile | 2 +- README.md | 122 ++++++++++++------ config.json | 7 +- src/kube/ingress.ts | 37 +++++- src/kube/service.ts | 30 ++++- src/kube/volume.ts | 27 +++- src/main.ts | 8 +- tests/kube/ingress.test.ts | 31 ++++- tests/kube/service.test.ts | 45 ++++++- tests/kube/volume.test.ts | 258 +++++++++++++++++++++++++++++++++++++ 11 files changed, 506 insertions(+), 66 deletions(-) create mode 100644 tests/kube/volume.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c39411e..e6f6c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ -## 1.1.0 +## 2.0.0 +**Maintainer**: Raffael Sahli \ +**Date**: Thur 14 Feb 2019 14:28:20 PM ET * [FIX] Fixes "(node:25116) UnhandledPromiseRejectionWarning: TypeError: result is not iterable" due error response from icinga * [FIX] Better error handling and catching various uncaught promises @@ -15,6 +17,7 @@ * [FIX] kube workes are in no namespace, remove icinga groups * [FIX] fixed Error: Invalid attribute specified: host_name\n for kube nodes * [FIX] fixed duplicate ingress objects (different path, same ingress names in different namespaces, ...) +* [CHANGE] Added hostName setting, by default all resources (except nodes) get attached to single host object of their type (can be configured differently) ## 1.0.1 diff --git a/Dockerfile b/Dockerfile index f3e5b77..4b15753 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8.10.0-alpine +FROM node:10.15.1-alpine RUN apk update && apk add git diff --git a/README.md b/README.md index c312ebc..1b70c5e 100644 --- a/README.md +++ b/README.md @@ -13,31 +13,28 @@ custom icinga objects or disable/enable kubernetes objects to monitor. * Autodiscovery * Icinga servicegroup support * Create services for kubernetes nodes, services, ingresses and persistent volumes -* Completely customizable per resurce or per resource type +* Completely customizable per resource or per resource type ## How does it work? Multiple watchers are bootstraped and listen for any kubernetes changes. Those changes will reflect immediately on your icinga environment. -* Kubernetes namespaces will result in icinga service groups and host groups +* Kubernetes namespaces will result in icinga service groups * Nodes will result in host objects -* Ingresses will result in dummy icinga host objects [1] (hostname is taken from metadata.name) whereas ingress paths will get deployed as icinga services related to the ingress host object -* Services (ClusterIP, NodePort [2], LoadBalanacer) will also result in icinga host objects and service ports are icinga services. -* Persistent volumes will result in services attached to a dummy host - ->**Note**: [1] You may change this behaviour by attaching the services (ingress paths) to all kubernetes worker nodes by setting `kubernetes.ingresses.attachToNodes` to `true`. (This will result in many more services and checks depending on the size of your cluster!) - ->**Note**: [2] NodePort services will always be attached to each kubernetes worker node. See [NodePort](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport) services for more information. - -Since there is no such thing as hosts in the world of moving and containers. The host object (kubernetes metadata.name) on icinga may just be a [dummy](https://www.icinga.com/docs/icinga2/latest/doc/10-icinga-template-library/#plugin-check-command-dummy) and will not get checked -if the service can not be attached to kubernetes nodes. - -You may wonder where your pods are in this setup. Well it does not make any sense to monitor single pods. Pods are mortal and (always) moving. The important thing is to monitor your services as an entity. +* Ingresses will result in icinga services (and or host objects) +* Services (ClusterIP, NodePort [2], LoadBalanacer) will result in icinga services (and/or host objects) +* Persistent volumes will result in icinga services (and/or host objects) ## Table of Contents -* [Description](#description) +* [Features](#features) * [How does it work?](#how-does-it-work) -* [Requirements](#requirements) +* [Resource types](#resource-types) + * [Namespaces](#namespaces) + * [Nodes](#nodes) + * [Ingresses](#ingresses) + * [Services](#services) + * [Volumes](#volumes) +* [Requirement](#requirements) * [Setup icinga2 api user](#setup-icinga2-api-user) * [Deployment](#deployment) * [Advanced topics](#advanced-topics) @@ -46,6 +43,50 @@ You may wonder where your pods are in this setup. Well it does not make any sens * [Overwite specific icinga object definition](#overwite-specific-icinga-object-definition) * [Configuration](#configuration) +## Resource types + +### Namespaces +Namespaces will result in icinga servicegroups. +Note that the servicegroup gets only created if one of the following resources exist in a given namespace. + +### Nodes +Kubernetes nodes get mappend 1:1 to icinga host objects. There is no other magic. +However you may change the icinga host object by configuring the global config `kubernetes.nodes` or +change annotations values in single node objects. + +### Ingresses +Each kubernetes ingress hostname/path will result in a single icinga service. By default all those services get +attached to a single icinga host named `kubernetes-ingresses` and each service also gets attached to a servicegroup which is taken from the kubernetes namespace. +You may change the hostname by configuring `kubernetes.ingresses.hostName` (or set a custom hostname in an annotation) or even set `kubernetes.ingresses.hostName` to `null`. +If hostName is `null`, host objects get dynamically created named `ingress-${namespace}-${name}`. This will result in more host objects but the services still get created and get attached to their servicegroup (namespace). +If you choose to create dynamic host objects you may want to disable service provisioning, you may set `kubernetes.ingresses.applyServices` to `false` and create services manually with icinga apply rules (see [advanced](#using-icinga2-apply-rules)). +Besides that you can still configure advanced options for those host objects and/or service objects (See configuration). +For instance you may attach ingress services directly to all kubernetes worker nodes by setting `kubernetes.ingresses.attachToNodes` to `true`. (This will result in many more services and checks depending on the size of your cluster!) + +By default ingresses get checked with the `http` check_command. Usually you do not want to change this but you can by using annotations or changing the global ingress settings. + +### Services +Each service port will result in a single icinga service (like ingress paths). By default all those services get +attached to a single host named `kubernetes-${serviceType}-services`. Supported service types are either ClusterIP, NodePort or LoadBalancer. +Like ingresses you may change the hostname or configure host provisioning by setting hostName to `false` (`kubernetes.services.${serviceType}.hostName`). +Dynamic service host objects get named `service-${namespace}-${name}`. You may also disable service provisioning by setting `kubernetes.services.${serviceType}.applyServices` to `false` if you prefer icinga apply rules. +ClusterIP provisioning is disabled by default since ClusterIP services are only visible internally in kubernetes. You need either an icinga deployment within the kubernetes cluster or optionally +an icinga [satelite](https://www.icinga.com/docs/icinga2/latest/doc/06-distributed-monitoring) if you decide to enable ClusterIP provisioning. +NodePort services get attached by default directly to the kubernetes icinga node objects. + +By default services get checked with the service protocol which usually result in check_tcp. However you may set custom settings using annotations (or global service settings). + +### Volumes +Each persistent volume will result in a single icinga service. By default all those services get attached to a single host named `kubernetes-volumes`. +Like for ingresses and services you may change the same advanced settings for volumes. +Since a persistent volume is not directly attached to namespace the namespace for the service group gets taken from the persistent volume claim. + +>**Note** Volume services are created by default with the dummy check_command. Since there is no way for kube-icinga to specify the correct command automatically. You need to either overwrite the check_command in annotations, set the command +in the serviceDefinition for volumes or disable service provisioning, enable host provisioning (set hostName to `null`) and create icinga apply rule based services manually. + +You may wonder where your pods are in this setup. Well it does not make any sense to monitor single pods. Pods are mortal and (always) moving. The important thing is to monitor your services as an entity. + + ## Requirements * A running kubernetes cluster * Icinga2 server with enabled API module @@ -78,9 +119,6 @@ You may specify resources visibile to kube-icinga with custom RBAC rules. ## Advanced topics -### ClusterIP services -Ingresses, NodePort and Load Balancer services will expose your kubernetes services to the public but not ClusterIP services. ClusterIP services (Which is the default) are only internal kubernetes services and not available from outside the cluster. If your icinga2 setup lives outside the cluster you only have two options, either disable deployment for those objects or setup an icinga [satelite](https://www.icinga.com/docs/icinga2/latest/doc/06-distributed-monitoring) container which will live on kubernetes. Of course you may also deploy an entire icinga2 setup on kubernetes if you do not have an existing one. - ### Using icinga2 apply rules You certainly can use icinga2 apply rules. You may disable auto service deployments via `applyServices` for ingresses, services and volumes and define your own services via [apply rules](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#using-apply). Of course you can also setup a mixed deployment. Automatically let kube-icinga deploy services and apply additional services via apply rules. @@ -89,16 +127,6 @@ Of course you can also setup a mixed deployment. Automatically let kube-icinga d All icinga host objects are created with all kubernetes data available packed in the variable `vars.kubernetes`. Therefore you may apply rules with this data. -For example kube-icinga will create a icinga host object for a kubernetes service: -``` - -``` - -You may use this data to dynamically create icinga apply rules: -``` -``` - - ### Globally overwrite icinga object definitions It is possible to set custom options for the icinga objects during creation. You might set custom values via `kubernetes.ingresses.hostTemplates` or kubernetes.ingresses.serviceTemplate`. The same can also be done for services. For example it might be crucial to set a custom zone for ClusterIP services since they are only reachable within the cluster and shall be monitored by an icinga satelite node. Any valid setting for a specific icinga object can be set. @@ -132,6 +160,7 @@ You may use the following annotations: | Name | Description | |-------|------------| | `kube-icinga/check_command` | Use a custom icinga check command. | +| `kube-icinga/host` | Use a custom hostname (to which icinga host a service gets bound to). | | `kube-icinga/template` | Use a custom icinga template. | | `kube-icinga/definition` | JSON encoded icinga definiton which may contain advanced icinga options and gets merged with the defaults. | @@ -140,16 +169,17 @@ You may use the following annotations: kind: PersistentVolume apiVersion: v1 metadata: - name: generic-nimble-24afe01f-2300-11e9-94e3-0050568fe3c2, - selfLink":"/api/v1/persistentvolumes/generic-nimble-24afe01f-2300-11e9-94e3-0050568fe3c2 + name: generic-storage-24afe01f-2300-11e9-94e3-0050568fe3c2, + selfLink":"/api/v1/persistentvolumes/generic-storage-24afe01f-2300-11e9-94e3-0050568fe3c2 uid":"2a66425e-2300-11e9-94e3-0050568fe3c2 resourceVersion: 65603642 creationTimestamp: 2019-01-28T13:25:22Z annotations: - kube-icinga/check_command: "check_nimble_lun" - hpe.com/docker-volume-name": "generic-nimble-24afe01f-2300-11e9-94e3-0050568fe3c2" - pv.kubernetes.io/provisioned-by: "hpe.com/nimble" - volume.beta.kubernetes.io/storage-class: "generic-nimble" + kube-icinga/check_command: "check_storage_lun" + kube-icinga/hostname: "storagesystem.example.lan" + hpe.com/docker-volume-name": "generic-storage-24afe01f-2300-11e9-94e3-0050568fe3c2" + pv.kubernetes.io/provisioned-by: "example.com/storage" + volume.beta.kubernetes.io/storage-class: "generic-storage" ``` ## Configuration @@ -169,26 +199,38 @@ List of configurable values: |`kubernetes.nodes.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_NODES_HOST_DEFINITION`|`{}`| |`kubernetes.nodes.hostTemplates`|Specify a list of host templates|`ICINGA_NODES_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.ingresses.discover`|Deploy kubernetes ingress objects|`KUBERNETES_INGRESSES_DISCOVER`|`true`| +|`kubernetes.ingresses.hostName`|The name of the icinga host object to attach services to (May also be null to enabled host object provisioning)|`KUBERNETES_INGRESSES_HOSTNAME`|`kubernetes-ingresses`| |`kubernetes.ingresses.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_INGRESSES_SERVICE_DEFINITION`|'{}'| |`kubernetes.ingresses.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_INGRESSES_SERVICE_DEFINITION`|`{}`| |`kubernetes.ingresses.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_INGRESSES_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.ingresses.hostTemplates`|Specify a list of host templates|`ICINGA_INGRESSES_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.ingresses.applyServices`|Apply ingress paths as icinga services attached to the icinga ingress host|`KUBERNETES_INGRESSES_APPLYSERVICES`|`true`| |`kubernetes.ingresses.attachToNodes`|If `true` instead attaching port services to a dummy host object `metadata.name` all services get attached to each kubernetes worker node!|`KUBERNETES_INGRESSES_ATTACHTONODES`|`false`| -|`kubernetes.services.ClusterIP.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_TYPE_DISCOVER`|`false`| -|`kubernetes.services.ClusterIP.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_SERVICE_DEFINITION`|`{}`| +|`kubernetes.volumes.discover`|Deploy kubernetes ingress objects|`KUBERNETES_VOLUMES_DISCOVER`|`true`| +|`kubernetes.volumes.hostName`|The name of the icinga host object to attach services to (May also be null to enabled host object provisioning)|`KUBERNETES_VOLUMES_HOSTNAME`|`kubernetes-volumes`| +|`kubernetes.volumes.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_VOLUMES_SERVICE_DEFINITION`|'{}'| +|`kubernetes.volumes.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_VOLUMES_SERVICE_DEFINITION`|`{}`| +|`kubernetes.volumes.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_VOLUMES_SERVICE_TEMPLATES`|`['generic-service']`| +|`kubernetes.volumes.hostTemplates`|Specify a list of host templates|`ICINGA_VOLUMES_HOST_TEMPLATES`|`['generic-host']`| +|`kubernetes.volumes.applyServices`|Apply volumes as icinga services attached to a icinga host object|`KUBERNETES_VOLUMES_APPLYSERVICES`|`true`| +|`kubernetes.volumes.attachToNodes`|If `true` instead attaching port services to a dummy host object `metadata.name` all services get attached to each kubernetes worker node!|`KUBERNETES_VOLUMES_ATTACHTONODES`|`false`| +|`kubernetes.services.ClusterIP.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_CLUSTERIP_DISCOVER`|`false`| +|`kubernetes.services.ClusterIP.hostName`|The name of the icinga host object to attach services to (May also be null to enabled host object provisioning)|`KUBERNETES_SERVICES_CLUSTERIP_HOSTNAME`|`kubernetes-clusterip-services`| +|`kubernetes.services.ClusterIP.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_CLUSTERIP_SERVICE_DEFINITION`|`{}`| |`kubernetes.services.ClusterIP.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_SERVICES_CLUSTERIP_HOST_DEFINITION`|`{}`| |`kubernetes.services.ClusterIP.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_SERVICES_CLUSTERIP_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.services.ClusterIP.hostTemplates`|Specify a list of host templates|`ICINGA_SERVICES_CLUSTERIP_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.services.ClusterIP.applyServices`|URI of LDAP server|`KUBERNETES_SERVICES_CLUSTERIP_APPLYSERVICES`|`true`| -|`kubernetes.services.NodePort.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_TYPE_DISCOVER`|`true`| +|`kubernetes.services.NodePort.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_NODEPORT_DISCOVER`|`true`| |`kubernetes.services.NodePort.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_SERVICE_DEFINITION`|`{}`| |`kubernetes.services.NodePort.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_SERVICES_NODEPORT_HOST_DEFINITION`|`{}`| |`kubernetes.services.NodePort.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_SERVICES_NODEPORT_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.services.NodePort.hostTemplates`|Specify a list of host templates|`ICINGA_SERVICES_NODEPORT_HOST_TEMPLATES`|`['generic-host']`| |`kubernetes.services.NodePort.applyServices`|URI of LDAP server|`KUBERNETES_SERVICES_NODEPORT_APPLYSERVICES`|`true`| -|`kubernetes.services.LoadBalancer.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_TYPE_DISCOVER`|`true`| -|`kubernetes.services.LoadBalancer.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_SERVICE_DEFINITION`|`{}`| +|`kubernetes.services.NodePort.hostName`|The name of the icinga host object to attach services to (May also be null to enabled host object provisioning)|`KUBERNETES_SERVICES_NODEPORT_HOSTNAME`|`kubernetes-nodeport-services`| +|`kubernetes.services.LoadBalancer.discover`|Deploy kubernetes service objects|`KUBERNETES_SERVICES_LOADBALANCER_DISCOVER`|`true`| +|`kubernetes.services.LoadBalancer.hostName`|The name of the icinga host object to attach services to (May also be null to enabled host object provisioning)|`KUBERNETES_SERVICES_LOADBALANCER_HOSTNAME`|`kubernetes-loadbalancer-services`| +|`kubernetes.services.LoadBalancer.serviceDefinition`|You may overwrite specific attributes of the icinga [service definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#service). |`ICINGA_SERVICES_LOADBALANCER_SERVICE_DEFINITION`|`{}`| |`kubernetes.services.LoadBalancer.hostDefinition`|You may overwrite specific attributes of the icinga [host definiton](https://www.icinga.com/docs/icinga2/latest/doc/09-object-types/#host).|`ICINGA_SERVICES_LOADBALANCER_HOST_DEFINITION`|`{}`| |`kubernetes.services.LoadBalancer.serviceTemplates`|Specify a list of icinga service templates|`ICINGA_SERVICES_LOADBALANCER_SERVICE_TEMPLATES`|`['generic-service']`| |`kubernetes.services.LoadBalancer.hostTemplates`|Specify a list of host templates|`ICINGA_SERVICES_LOADBALANCER_HOST_TEMPLATES`|`['generic-host']`| diff --git a/config.json b/config.json index e0a4db8..58595b5 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "log": { - "level": "debug" + "level": "info" }, "cleanup": true, "icinga": { @@ -17,6 +17,7 @@ }, "ingresses": { "discover": true, + "hostName": "kubernetes-ingresses", "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], @@ -26,6 +27,7 @@ }, "volumes": { "discover": true, + "hostName": "kubernetes-volumes", "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], @@ -35,6 +37,7 @@ "services": { "ClusterIP": { "discover": false, + "hostName": "kubernetes-clusterip-services", "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], @@ -42,6 +45,7 @@ }, "NodePort": { "discover": true, + "hostName": "kubernetes-nodeport-services", "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], @@ -51,6 +55,7 @@ }, "LoadBalancer": { "discover": true, + "hostName": "kubernetes-loadbalancer-services", "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index 0ee8df6..9e4eaaf 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -6,6 +6,7 @@ import Resource from './abstract.resource'; interface IngressOptions { discover?: boolean; + hostName?: string; applyServices?: boolean; attachToNodes?: boolean; hostDefinition?: object; @@ -24,6 +25,7 @@ export default class Ingress extends Resource { protected kubeNode: KubeNode; protected options = { applyServices: true, + hostName: "kubernetes-ingresses", attachToNodes: false, hostDefinition: {}, serviceDefinition: {}, @@ -79,9 +81,10 @@ export default class Ingress extends Resource { * Preapre icinga object and apply */ public async prepareObject(definition: any): Promise { - let hostname = this.escapeName(['ingress', definition.metadata.namespace, definition.metadata.name].join('-')); + let hostname = this.getHostname(definition); + if (!this.options.attachToNodes) { - await this.applyHost(hostname, definition.metadata.name, definition, this.options.hostTemplates); + await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); } let service = this.prepareResource(definition); @@ -123,14 +126,31 @@ export default class Ingress extends Resource { } } + /** + * Get hostname + */ + protected getHostname(definition: any): string { + if(definition.metadata.annotations['kube-icinga/host']) { + return definition.metadata.annotations['kube-icinga/host']; + } else if(this.options.hostName === null) { + return this.escapeName(['ingress', definition.metadata.namespace, definition.metadata.name].join('-')); + } + + return this.options.hostName; + } + /** * Start kube listener */ public async kubeListener(provider) { +console.log("Start listener before try"); try { let stream = provider(); + console.log("Start listener"); + console.log(stream); stream.pipe(this.jsonStream); this.jsonStream.on('data', async (object) => { + console.log("data"); this.logger.debug('received kubernetes ingress resource', {object}); if (object.object.kind !== 'Ingress') { @@ -138,8 +158,10 @@ export default class Ingress extends Resource { return; } + let hostname = this.escapeName(['ingress', object.object.metadata.namespace, object.object.metadata.name].join('-')); if (object.type == 'MODIFIED' || object.type == 'DELETED') { - await this.icinga.deleteHost(object.object.metadata.name); + let hostname = this.getHostname(object.object); + await this.icinga.deleteHost(hostname); } if (object.type == 'ADDED' || object.type == 'MODIFIED') { @@ -150,6 +172,15 @@ export default class Ingress extends Resource { }); this.jsonStream.on('finish', () => { + console.log("finish", this.kubeListener, provider); + this.kubeListener(provider); + }); + this.jsonStream.on('error', () => { + console.log("error", this.kubeListener, provider); + this.kubeListener(provider); + }); + this.jsonStream.on('end', () => { + console.log("end", this.kubeListener, provider); this.kubeListener(provider); }); } catch (err) { diff --git a/src/kube/service.ts b/src/kube/service.ts index 6949953..1a0a3db 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -6,6 +6,7 @@ import Resource from './abstract.resource'; interface ServiceTypeOptions { discover?: boolean; + hostName?: string; applyServices?: boolean; hostDefinition?: any; serviceDefinition?: any; @@ -22,6 +23,7 @@ interface ServiceOptions { const defaults: ServiceOptions = { ClusterIP: { discover: false, + hostName: "kubernetes-clusterip-services", applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -30,6 +32,7 @@ const defaults: ServiceOptions = { }, NodePort: { discover: true, + hostName: "kubernetes-nodeport-services", applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -38,6 +41,7 @@ const defaults: ServiceOptions = { }, LoadBalancer: { discover: true, + hostName: "kubernetes-loadbalancer-services", applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -122,11 +126,13 @@ export default class Service extends Resource { service['groups'] = [definition.metadata.namespace]; Object.assign(service, this.prepareResource(definition)); - let hostname = this.escapeName(['service', definition.metadata.namespace, definition.metadata.name].join('-')); + let hostname = this.getHostname(definition); let templates = options.serviceTemplates; templates = templates.concat(this.prepareTemplates(definition)); + if (serviceType !== Service.TYPE_NODEPORT) { - await this.applyHost(hostname, definition.spec.clusterIP, serviceType, definition, options.hostTemplates); + let address = options.hostName || definition.spec.clusterIP; + await this.applyHost(hostname, address, serviceType, definition, options.hostTemplates); } if (options.applyServices) { @@ -155,7 +161,7 @@ export default class Service extends Resource { port['vars._kubernetes'] = true; port['vars.kubernetes'] = definition; - let name = this.escapeName([definition.metadata.name, portName].join('-')); + let name = this.escapeName([definition.metadata.namespace, definition.metadata.name, portName].join('-')); port['display_name'] = name; this.applyService(hostname, name, serviceType, port, templates); @@ -163,6 +169,21 @@ export default class Service extends Resource { } } + /** + * Get hostname + */ + protected getHostname(definition: any): string { + let serviceType = definition.spec.type; + + if(definition.metadata.annotations['kube-icinga/host']) { + return definition.metadata.annotations['kube-icinga/host']; + } else if(this.options[serviceType].hostName === null) { + return this.escapeName(['service', definition.metadata.namespace, definition.metadata.name].join('-')); + } + + return this.options[serviceType].hostName; + } + /** * Start kube listener */ @@ -184,7 +205,8 @@ export default class Service extends Resource { } if (object.type == 'MODIFIED' || object.type == 'DELETED') { - await this.icinga.deleteHost(object.object.metadata.name); + let hostname = this.getHostname(object.object); + await this.icinga.deleteHost(hostname); } if (object.type == 'ADDED' || object.type == 'MODIFIED') { diff --git a/src/kube/volume.ts b/src/kube/volume.ts index e7e9e80..78c55d0 100644 --- a/src/kube/volume.ts +++ b/src/kube/volume.ts @@ -8,6 +8,7 @@ interface VolumeOptions { discover?: boolean; applyServices?: boolean; attachToNodes?: boolean; + hostName?: string; hostDefinition?: object; serviceDefinition?: object; hostTemplates?: string[]; @@ -25,6 +26,7 @@ export default class Volume extends Resource { protected options = { applyServices: true, attachToNodes: false, + hostName: "kubernetes-volumes", hostDefinition: {}, serviceDefinition: {}, hostTemplates: [], @@ -79,8 +81,11 @@ export default class Volume extends Resource { * Preapre icinga object and apply */ public async prepareObject(definition: any): Promise { - let hostname = this.escapeName(definition.metadata.annotations['pv.kubernetes.io/provisioned-by']); - await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); + var hostname = this.getHostname(definition); + + if (!this.options.attachToNodes) { + await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); + } if (this.options.applyServices) { let groups = []; @@ -93,7 +98,7 @@ export default class Volume extends Resource { templates = templates.concat(this.prepareTemplates(definition)); let service = this.options.serviceDefinition; - let name = this.escapeName(['volume', definition.metadata.name].join('-')); + let name = this.escapeName(definition.metadata.name); let addition = { 'check_command': 'dummy', 'display_name': `${definition.metadata.name}:volume`, @@ -108,6 +113,19 @@ export default class Volume extends Resource { } } + /** + * Get hostname + */ + protected getHostname(definition: any): string { + if(definition.metadata.annotations['kube-icinga/host']) { + return definition.metadata.annotations['kube-icinga/host']; + } else if(this.options.hostName === null) { + return this.escapeName(['volume', definition.metadata.name].join('-')); + } + + return this.options.hostName; + } + /** * Start kube listener */ @@ -124,7 +142,8 @@ export default class Volume extends Resource { } if (object.type == 'MODIFIED' || object.type == 'DELETED') { - await this.icinga.deleteHost(object.object.metadata.name); + let hostname = this.getHostname(object.object); + await this.icinga.deleteHost(this.options.hostName); } if (object.type == 'ADDED' || object.type == 'MODIFIED') { diff --git a/src/main.ts b/src/main.ts index bf027a8..dacf946 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,19 +26,19 @@ async function main() { } if (config.kubernetes.nodes.discover) { - kubeNode.kubeListener(() => { + kubeNode.kubeListener(function() { return kubeClient.apis.v1.watch.nodes.getStream(); }); } if (config.kubernetes.ingresses.discover) { - kubeIngress.kubeListener(() => { + kubeIngress.kubeListener(function() { return kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); }); } if (config.kubernetes.volumes.discover) { - kubeVolume.kubeListener(() => { + kubeVolume.kubeListener(function() { return kubeClient.apis.v1.watch.persistentvolumes.getStream(); }); } @@ -46,7 +46,7 @@ async function main() { if (config.kubernetes.services.ClusterIP.discover || config.kubernetes.services.NodePort.discover || config.kubernetes.services.LoadBalancer.discover) { - kubeService.kubeListener(() => { + kubeService.kubeListener(function() { return kubeClient.apis.v1.watch.services.getStream(); }); } diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index 8d575c8..cb3feb5 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -68,6 +68,19 @@ describe('kubernetes ingresses', () => { Icinga.applyHost = jest.fn(); instance.prepareObject(fixture); const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('kubernetes-ingresses'); + expect(call[1].display_name).toBe('kubernetes-ingresses'); + expect(call[1].check_command).toBe('dummy'); + }); + + it('create icinga host object with dynamic host', () => { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, + hostName: null + }); + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; expect(call[0]).toBe('ingress-foobar-foo'); expect(call[1].display_name).toBe('ingress-foobar-foo'); expect(call[1].check_command).toBe('dummy'); @@ -146,13 +159,27 @@ describe('kubernetes ingresses', () => { await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; expect(Icinga.applyService.mock.instances.length).toBe(2); - expect(calls[0][0]).toBe('ingress-foobar-foo'); - expect(calls[1][0]).toBe('ingress-foobar-foo'); + expect(calls[0][0]).toBe('kubernetes-ingresses'); + expect(calls[1][0]).toBe('kubernetes-ingresses'); expect(calls[0][1]).toBe('foobar.example.org-http--'); expect(calls[1][1]).toBe('barfoo.example.org-http--foo'); expect(calls[0][2]['vars.http_path']).toBe('/'); expect(calls[1][2]['vars.http_path']).toBe('/foo'); }); + + it('create all service objects with dynamic hosts', async () => { + let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + hostName: null + }); + + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][0]).toBe('ingress-foobar-foo'); + expect(calls[1][0]).toBe('ingress-foobar-foo'); + } it('create all service objects with custom service definition', async () => { let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts index 90d596c..6dd926f 100644 --- a/tests/kube/service.test.ts +++ b/tests/kube/service.test.ts @@ -52,9 +52,26 @@ beforeEach(() => { describe('kubernetes services', () => { describe('add service object with dummy host', () => { it('create icinga host object', () => { - let instance = new Service(Logger, Node, Icinga, { + let instance = new Service(Logger, Node, Icinga, JSONStream, { ClusterIP: { - applyServices: false + applyServices: false + } + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('kubernetes-clusterip-services'); + expect(call[1].address).toBe('kubernetes-clusterip-services'); + expect(call[1].display_name).toBe('kubernetes-clusterip-services'); + expect(call[1].check_command).toBe('dummy'); + }); + + it('create dynamic icinga host object', () => { + let instance = new Service(Logger, Node, Icinga, JSONStream, { + ClusterIP: { + hostName: null, + applyServices: false } }); @@ -149,16 +166,32 @@ describe('kubernetes services', () => { await instance.prepareObject(fixture); const calls = Icinga.applyService.mock.calls; expect(Icinga.applyService.mock.instances.length).toBe(2); - expect(calls[0][0]).toBe('service-foobar-foo'); - expect(calls[1][0]).toBe('service-foobar-foo'); - expect(calls[0][1]).toBe('foo-http'); - expect(calls[1][1]).toBe('foo-bar'); + expect(calls[0][0]).toBe('kubernetes-clusterip-services'); + expect(calls[1][0]).toBe('kubernetes-clusterip-services'); + expect(calls[0][1]).toBe('foobar-foo-http'); + expect(calls[1][1]).toBe('foobar-foo-bar'); expect(calls[0][2]['check_command']).toBe('tcp'); expect(calls[0][2]['vars.tcp_port']).toBe(80); expect(calls[1][2]['check_command']).toBe('tcp'); expect(calls[1][2]['vars.tcp_port']).toBe(10000); }); + + it('create all service objects with dynamic hosts', async () => { + let instance = new Service(Logger, Node, Icinga, JSONStream, { + ClusterIP: { + hostName: null + } + }); + + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(2); + expect(calls[0][0]).toBe('service-foobar-foo'); + expect(calls[1][0]).toBe('service-foobar-foo'); + }); it('create all service objects with custom service definition', async () => { let instance = new Service(Logger, Node, Icinga, JSONStream, { diff --git a/tests/kube/volume.test.ts b/tests/kube/volume.test.ts new file mode 100644 index 0000000..2dff1be --- /dev/null +++ b/tests/kube/volume.test.ts @@ -0,0 +1,258 @@ +import Volume from '../../src/kube/volume'; +import Node from '../../src/kube/node'; +import Icinga from '../../src/icinga'; +import {LoggerInstance} from 'winston'; +import * as JSONStream from 'json-stream'; +jest.mock('../../src/icinga'); +jest.mock('json-stream'); +jest.mock('kubernetes-client'); + +const template = { + "apiVersion": "v1", + "kind": "PersistentVolume", + "metadata": { + "annotations": { + "hpe.com/docker-volume-name": "generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2", + "pv.kubernetes.io/provisioned-by": "hpe.com/nimble", + "volume.beta.kubernetes.io/storage-class": "generic-nimble" + }, + "name": "generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2", + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "capacity": { + "storage": "50Gi" + }, + "claimRef": { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "name": "nimbletestpvc-006", + "namespace": "foobar" + }, + "flexVolume": { + "driver": "hpe.com/nimble", + "options": { + "name": "generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2", + "thick": "false" + } + }, + "persistentVolumeReclaimPolicy": "Delete", + "storageClassName": "generic-nimble" + }, + "status": { + "phase": "Bound" + } +}; + +var fixture; + +beforeEach(() => { + fixture = JSON.parse(JSON.stringify(template)); +}); + +describe('kubernetes volumes', () => { + var instance: Volume; + + describe('add volume object with dummy host', () => { + it('create icinga host object', () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false + }); + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('kubernetes-volumes'); + expect(call[1].display_name).toBe('kubernetes-volumes'); + expect(call[1].check_command).toBe('dummy'); + }); + + it('create icinga host object with dynamic host', () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, + hostName: null + }); + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[0]).toBe('volume-generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + expect(call[1].display_name).toBe('volume-generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + expect(call[1].check_command).toBe('dummy'); + }); + + it('create icinga host object with custom definitions', () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, + hostDefinition: { + 'vars.foo': 'bar', + 'vars.check_command': 'foo' + } + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[1]['vars.foo']).toBe('bar'); + expect(call[1]['vars.check_command']).toBe('foo'); + }); + + it('create icinga host object with templates', () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, + hostTemplates: ['foo', 'bar'] + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyHost.mock.calls[0]; + expect(call[2]).toEqual(['foo', 'bar']); + }); + + it('do not create icinga host object while attachToNodes is enabled', () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false, + attachToNodes: true + }); + + Icinga.applyHost = jest.fn(); + instance.prepareObject(fixture); + expect(Icinga.applyHost.mock.instances.length).toBe(0); + }); + }); + + describe('add volume object namespace as service group', () => { + it('create service group per default', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream); + + Icinga.applyHost = jest.fn(); + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + await instance.prepareObject(fixture); + const call = Icinga.applyServiceGroup.mock.calls[0]; + expect(call[0]).toBe('foobar'); + }); + + it('do not create servicegroup if applyServices is disabled', () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: false + }); + + Icinga.applyServiceGroup = jest.fn(); + instance.prepareObject(fixture); + const call = Icinga.applyServiceGroup.mock.instances[0]; + expect(Icinga.applyServiceGroup.mock.instances.length).toBe(0); + }); + }); + + describe('add all volume object http path rules as service objects', () => { + it('create service object', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream); + + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][0]).toBe('kubernetes-volumes'); + expect(calls[0][1]).toBe('generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + }); + + it('create service object with dynamic host', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + hostName: null + }); + + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][0]).toBe('volume-generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + } + + it('create all service objects with custom service definition', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: true, + serviceDefinition: { + 'check_command': 'tcp', + 'vars.foo': 'bar' + } + }); + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][2].check_command).toBe('tcp'); + expect(calls[0][2]['vars.foo']).toBe('bar'); + }); + + it('create all service objects with templates', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: true, + serviceTemplates: ['foo', 'bar'] + }); + + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][3]).toEqual(['foo', 'bar']); + }); + }); + + describe('kubernetes annotations', () => { + it('check_command/templates annotation', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: true + }); + + fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + fixture.metadata.annotations['kube-icinga/templates'] = 'foobar,barfoo'; + + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][2].check_command).toBe('bar'); + expect(calls[0][3]).toEqual(['foobar', 'barfoo']); + }); + + it('use annotation instead global definition', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: true, + serviceDefinition: { + check_command: 'foo' + } + }); + fixture.metadata.annotations['kube-icinga/check_command'] = 'bar'; + + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][2].check_command).toBe('bar'); + }); + + it('definiton merge', async () => { + let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + applyServices: true, + serviceDefinition: { + check_command: 'foo' + } + }); + fixture.metadata.annotations['kube-icinga/definition'] = '{"vars.foo": "foobar"}'; + + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyService = jest.fn(); + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyService.mock.instances.length).toBe(1); + expect(calls[0][2]["check_command"]).toBe('foo'); + expect(calls[0][2]["vars.foo"]).toBe('foobar'); + }); + }); +}); From 986709df5e42119fc7e4771d4427591dbdd98797 Mon Sep 17 00:00:00 2001 From: raffis Date: Fri, 15 Feb 2019 15:27:10 +0100 Subject: [PATCH 10/15] v2.0.0 fixes --- config.json | 6 ++--- src/config.ts | 19 +++++++++------ src/icinga.ts | 45 +++++++++++++++++++++++++++++++++- src/kube/ingress.ts | 50 ++++++++++++++++++-------------------- src/kube/node.ts | 12 +++------ src/kube/service.ts | 39 ++++++++++++++++++----------- src/kube/volume.ts | 37 +++++++++++++++++----------- src/main.ts | 40 +++++++++++++++++++++--------- tests/kube/ingress.test.ts | 32 ++++++++++++------------ tests/kube/node.test.ts | 4 +-- tests/kube/service.test.ts | 32 ++++++++++++------------ tests/kube/volume.test.ts | 30 +++++++++++------------ 12 files changed, 208 insertions(+), 138 deletions(-) diff --git a/config.json b/config.json index 58595b5..3d2637c 100644 --- a/config.json +++ b/config.json @@ -41,7 +41,7 @@ "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], - "applyServices": false + "applyServices": true }, "NodePort": { "discover": true, @@ -50,7 +50,7 @@ "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], - "applyServices": false, + "applyServices": true, "attachToNodes": false }, "LoadBalancer": { @@ -60,7 +60,7 @@ "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], - "applyServices": false + "applyServices": true } } } diff --git a/src/config.ts b/src/config.ts index 1a9ab6c..cd9164e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,6 +26,7 @@ const config = { }, ingresses: { discover: process.env.KUBERNETES_INGRESSES_DISCOVER || defaultConfig.kubernetes.ingresses.discover || true, + hostName: process.env.KUBERNETES_INGRESSES_HOSTNAME || defaultConfig.kubernetes.ingresses.hostName || 'kubernetes-ingresses', applyServices: process.env.KUBERNETES_INGRESSES_APPLYSERVICES || defaultConfig.kubernetes.ingresses.applyServices || true, serviceDefinition: process.env.KUBERNETES_INGRESSES_SERVICE_DEFINITION || defaultConfig.kubernetes.ingresses.serviceDefinition || {}, hostDefinition: process.env.KUBERNETES_INGRESSES_HOST_DEFINITION || defaultConfig.kubernetes.ingresses.hostDefinition || {}, @@ -34,17 +35,19 @@ const config = { attachToNodes: process.env.KUBERNETES_INGRESSES_ATTACHTONODES || defaultConfig.kubernetes.ingresses.attachToNodes || false, }, volumes: { - discover: process.env.KUBERNETES_INGRESSES_DISCOVER || defaultConfig.kubernetes.volumes.discover || true, - applyServices: process.env.KUBERNETES_INGRESSES_APPLYSERVICES || defaultConfig.kubernetes.volumes.applyServices || true, - serviceDefinition: process.env.KUBERNETES_INGRESSES_SERVICE_DEFINITION || defaultConfig.kubernetes.volumes.serviceDefinition || {}, - hostDefinition: process.env.KUBERNETES_INGRESSES_HOST_DEFINITION || defaultConfig.kubernetes.volumes.hostDefinition || {}, - serviceTemplates: process.env.KUBERNETES_INGRESSES_SERVICE_TEMPLATES || defaultConfig.kubernetes.volumes.serviceTemplates || ['generic-service'], - hostTemplates: process.env.KUBERNETES_INGRESSES_HOST_TEMPLATES || defaultConfig.kubernetes.volumes.hostTemplates || ['generic-host'], - attachToNodes: process.env.KUBERNETES_INGRESSES_ATTACHTONODES || defaultConfig.kubernetes.volumes.attachToNodes || false, + discover: process.env.KUBERNETES_VOLUMES_DISCOVER || defaultConfig.kubernetes.volumes.discover || true, + hostName: process.env.KUBERNETES_VOLUMES_HOSTNAME || defaultConfig.kubernetes.volumes.hostName || 'kubernetes-volumes', + applyServices: process.env.KUBERNETES_VOLUMES_APPLYSERVICES || defaultConfig.kubernetes.volumes.applyServices || true, + serviceDefinition: process.env.KUBERNETES_VOLUMES_SERVICE_DEFINITION || defaultConfig.kubernetes.volumes.serviceDefinition || {}, + hostDefinition: process.env.KUBERNETES_VOLUMES_HOST_DEFINITION || defaultConfig.kubernetes.volumes.hostDefinition || {}, + serviceTemplates: process.env.KUBERNETES_VOLUMES_SERVICE_TEMPLATES || defaultConfig.kubernetes.volumes.serviceTemplates || ['generic-service'], + hostTemplates: process.env.KUBERNETES_VOLUMES_HOST_TEMPLATES || defaultConfig.kubernetes.volumes.hostTemplates || ['generic-host'], + attachToNodes: process.env.KUBERNETES_VOLUMES_ATTACHTONODES || defaultConfig.kubernetes.volumes.attachToNodes || false, }, services: { ClusterIP: { discover: process.env.KUBERNETES_SERVICES_CLUSTERIP_DISCOVER || defaultConfig.kubernetes.services.ClusterIP.discover || false, + hostName: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOSTNAME || defaultConfig.kubernetes.services.ClusterIP.hostName || 'kubernetes-clusterip-services', applyServices: process.env.KUBERNETES_SERVICES_CLUSTERIP_APPLYSERVICES || defaultConfig.kubernetes.services.ClusterIP.applyServices || true, serviceDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_SERVICE_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.serviceDefinition || {}, hostDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.hostDefinition || {}, @@ -53,6 +56,7 @@ const config = { }, NodePort: { discover: process.env.KUBERNETES_SERVICES_NODEPORT_DISCOVER || defaultConfig.kubernetes.services.NodePort.discover || true, + hostName: process.env.KUBERNETES_SERVICES_NODEPORT_HOSTNAME || defaultConfig.kubernetes.services.NodePort.hostName || 'kubernetes-nodeport-services', applyServices: process.env.KUBERNETES_SERVICES_NODEPORT_APPLYSERVICES || defaultConfig.kubernetes.services.NodePort.applyServices || true, serviceDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_SERVICE_DEFINITION || defaultConfig.kubernetes.services.NodePort.serviceDefinition || {}, hostDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_DEFINITION || defaultConfig.kubernetes.services.NodePort.hostDefinition || {}, @@ -61,6 +65,7 @@ const config = { }, LoadBalancer: { discover: process.env.KUBERNETES_SERVICES_LOADBALANCER_DISCOVER || defaultConfig.kubernetes.services.LoadBalancer.discover || true, + hostName: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOSTNAME || defaultConfig.kubernetes.services.LoadBalancer.hostName || 'kubernetes-loadbalancer-services', applyServices: process.env.KUBERNETES_SERVICES_LOADBALANCER_APPLYSERVICES || defaultConfig.kubernetes.services.LoadBalancer.applyServices || true, serviceDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_SERVICE_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.serviceDefinition || {}, hostDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.hostDefinition || {}, diff --git a/src/icinga.ts b/src/icinga.ts index c698531..e3793e2 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -210,7 +210,7 @@ export default class Icinga { /** * Cleanup all previously deployed icinga kubernetes objects */ - public async cleanup(): Promise { + /*public async cleanup(): Promise { this.logger.info('start cleanup, removing all kubernetes objects from icinga'); return new Promise((resolve, reject) => { @@ -238,6 +238,49 @@ export default class Icinga { handlers.push(this.deleteHost(host.attrs.name)); } + await Promise.all(handlers); + resolve(true); + }); + }); + }*/ + + /** + * Delete services by filter + */ + public async deleteServicesByFilter(filter: string): Promise { + return new Promise((resolve, reject) => { + this.icingaClient.getServiceFiltered({filter: filter}, async (err, result) => { +console.log(err, result); + if (err) { + return reject(err); + } + + let handlers = []; + for (const service of result) { + handlers.push(this.deleteService(service.attrs.host_name, service.attrs.name)); + } + + await Promise.all(handlers); + resolve(true); + }); + }); + } + + /** + * Delete hosts by filter + */ + public async deleteHostsByFilter(filter: string): Promise { + return new Promise((resolve, reject) => { + this.icingaClient.getHostFiltered({filter: filter}, async (err, result) => { + if (err) { + return reject(err); + } + + let handlers = []; + for (const host of result) { + handlers.push(this.deleteHost(host.attrs.name)); + } + await Promise.all(handlers); resolve(true); }); diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index 9e4eaaf..f2308a9 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -1,6 +1,5 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; -import JSONStream from 'json-stream'; import KubeNode from './node'; import Resource from './abstract.resource'; @@ -21,11 +20,10 @@ interface IngressOptions { export default class Ingress extends Resource { protected logger: Logger; protected icinga: Icinga; - protected jsonStream: JSONStream; protected kubeNode: KubeNode; protected options = { applyServices: true, - hostName: "kubernetes-ingresses", + hostName: 'kubernetes-ingresses', attachToNodes: false, hostDefinition: {}, serviceDefinition: {}, @@ -36,11 +34,10 @@ export default class Ingress extends Resource { /** * kubernetes hosts */ - constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, jsonStream: JSONStream, options: IngressOptions) { + constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, options: IngressOptions) { super(); this.logger = logger; this.icinga = icinga; - this.jsonStream = jsonStream; this.kubeNode = kubeNode; this.options = Object.assign(this.options, options); } @@ -102,6 +99,7 @@ export default class Ingress extends Resource { 'check_command': 'http', 'display_name': `${spec.host}${base}:http`, 'vars._kubernetes': true, + 'vars._kubernetes_uid': definition.metadata.uid, 'vars.kubernetes': definition, 'vars.http_address': spec.host, 'vars.http_vhost': spec.host, @@ -130,27 +128,34 @@ export default class Ingress extends Resource { * Get hostname */ protected getHostname(definition: any): string { - if(definition.metadata.annotations['kube-icinga/host']) { + if (definition.metadata.annotations['kube-icinga/host']) { return definition.metadata.annotations['kube-icinga/host']; - } else if(this.options.hostName === null) { + } else if (this.options.hostName === null) { return this.escapeName(['ingress', definition.metadata.namespace, definition.metadata.name].join('-')); } return this.options.hostName; - } + } + + /** + * Delete object + */ + protected deleteObject(definition: any): Promise { + if (this.options.hostName === null) { + let hostname = this.getHostname(definition); + return this.icinga.deleteHost(hostname); + } + + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); + } /** * Start kube listener */ public async kubeListener(provider) { -console.log("Start listener before try"); try { let stream = provider(); - console.log("Start listener"); - console.log(stream); - stream.pipe(this.jsonStream); - this.jsonStream.on('data', async (object) => { - console.log("data"); + stream.on('data', async (object) => { this.logger.debug('received kubernetes ingress resource', {object}); if (object.object.kind !== 'Ingress') { @@ -158,10 +163,10 @@ console.log("Start listener before try"); return; } - let hostname = this.escapeName(['ingress', object.object.metadata.namespace, object.object.metadata.name].join('-')); if (object.type == 'MODIFIED' || object.type == 'DELETED') { - let hostname = this.getHostname(object.object); - await this.icinga.deleteHost(hostname); + await this.deleteObject(object.object).catch((err) => { + this.logger.error('failed to remove objects', {error: err}) + }); } if (object.type == 'ADDED' || object.type == 'MODIFIED') { @@ -171,16 +176,7 @@ console.log("Start listener before try"); } }); - this.jsonStream.on('finish', () => { - console.log("finish", this.kubeListener, provider); - this.kubeListener(provider); - }); - this.jsonStream.on('error', () => { - console.log("error", this.kubeListener, provider); - this.kubeListener(provider); - }); - this.jsonStream.on('end', () => { - console.log("end", this.kubeListener, provider); + stream.on('finish', () => { this.kubeListener(provider); }); } catch (err) { diff --git a/src/kube/node.ts b/src/kube/node.ts index c0a7a32..ed7c759 100644 --- a/src/kube/node.ts +++ b/src/kube/node.ts @@ -1,6 +1,5 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; -import JSONStream from 'json-stream'; import Resource from './abstract.resource'; interface NodeOptions { @@ -15,7 +14,6 @@ interface NodeOptions { export default class Node extends Resource { protected logger: Logger; protected icinga: Icinga; - protected jsonStream: JSONStream; protected nodes: string[] = []; protected options: NodeOptions = { discover: true, @@ -26,11 +24,10 @@ export default class Node extends Resource { /** * kubernetes hosts */ - constructor(logger: Logger, icinga: Icinga, jsonStream: JSONStream, options: NodeOptions) { + constructor(logger: Logger, icinga: Icinga, options: NodeOptions) { super(); this.logger = logger; this.icinga = icinga; - this.jsonStream = jsonStream; this.options = Object.assign(this.options, options); } @@ -67,9 +64,8 @@ export default class Node extends Resource { */ public async kubeListener(provider) { try { - const stream = provider(); - stream.pipe(this.jsonStream); - this.jsonStream.on('data', async (object) => { + let stream = provider(); + stream.on('data', async (object) => { // ignore MODIFIER for kube nodes if (object.type === 'MODIFIED') { return; @@ -92,7 +88,7 @@ export default class Node extends Resource { } }); - this.jsonStream.on('finish', () => { + stream.on('finish', () => { this.kubeListener(provider); }); } catch (err) { diff --git a/src/kube/service.ts b/src/kube/service.ts index 1a0a3db..d0cda80 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -1,6 +1,5 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; -import JSONStream from 'json-stream'; import KubeNode from './node'; import Resource from './abstract.resource'; @@ -23,7 +22,7 @@ interface ServiceOptions { const defaults: ServiceOptions = { ClusterIP: { discover: false, - hostName: "kubernetes-clusterip-services", + hostName: 'kubernetes-clusterip-services', applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -32,7 +31,7 @@ const defaults: ServiceOptions = { }, NodePort: { discover: true, - hostName: "kubernetes-nodeport-services", + hostName: 'kubernetes-nodeport-services', applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -41,7 +40,7 @@ const defaults: ServiceOptions = { }, LoadBalancer: { discover: true, - hostName: "kubernetes-loadbalancer-services", + hostName: 'kubernetes-loadbalancer-services', applyServices: true, hostDefinition: {}, serviceDefinition: {}, @@ -60,18 +59,16 @@ export default class Service extends Resource { protected logger: Logger; protected icinga: Icinga; - protected jsonStream: JSONStream; protected kubeNode: KubeNode; protected options: ServiceOptions = defaults; /** * kubernetes services */ - constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, jsonStream: JSONStream, options: ServiceOptions=defaults) { + constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, options: ServiceOptions=defaults) { super(); this.logger = logger; this.icinga = icinga; - this.jsonStream = jsonStream; let clone = JSON.parse(JSON.stringify(defaults)); Object.assign(clone.ClusterIP, options.ClusterIP); Object.assign(clone.NodePort, options.NodePort); @@ -175,23 +172,36 @@ export default class Service extends Resource { protected getHostname(definition: any): string { let serviceType = definition.spec.type; - if(definition.metadata.annotations['kube-icinga/host']) { + if (definition.metadata.annotations['kube-icinga/host']) { return definition.metadata.annotations['kube-icinga/host']; - } else if(this.options[serviceType].hostName === null) { + } else if (this.options[serviceType].hostName === null) { return this.escapeName(['service', definition.metadata.namespace, definition.metadata.name].join('-')); } return this.options[serviceType].hostName; } + /** + * Delete object + */ + protected deleteObject(definition: any): Promise { + let serviceType = definition.spec.type; + + if (this.options[serviceType].hostName === null) { + let hostname = this.getHostname(definition); + return this.icinga.deleteHost(hostname); + } + + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); + } + /** * Start kube listener */ public async kubeListener(provider) { try { let stream = provider(); - stream.pipe(this.jsonStream); - this.jsonStream.on('data', async (object) => { + stream.on('data', async (object) => { this.logger.debug('received kubernetes service resource', {object}); if (object.object.kind !== 'Service') { @@ -205,8 +215,9 @@ export default class Service extends Resource { } if (object.type == 'MODIFIED' || object.type == 'DELETED') { - let hostname = this.getHostname(object.object); - await this.icinga.deleteHost(hostname); + await this.deleteObject(object.object).catch((err) => { + this.logger.error('failed to remove objects', {error: err}) + }); } if (object.type == 'ADDED' || object.type == 'MODIFIED') { @@ -216,7 +227,7 @@ export default class Service extends Resource { } }); - this.jsonStream.on('finish', () => { + stream.on('finish', () => { this.kubeListener(provider); }); } catch (err) { diff --git a/src/kube/volume.ts b/src/kube/volume.ts index 78c55d0..f1d64ea 100644 --- a/src/kube/volume.ts +++ b/src/kube/volume.ts @@ -1,6 +1,5 @@ import {Logger} from 'winston'; import Icinga from '../icinga'; -import JSONStream from 'json-stream'; import KubeNode from './node'; import Resource from './abstract.resource'; @@ -21,12 +20,11 @@ interface VolumeOptions { export default class Volume extends Resource { protected logger: Logger; protected icinga: Icinga; - protected jsonStream: JSONStream; protected kubeNode: KubeNode; protected options = { applyServices: true, attachToNodes: false, - hostName: "kubernetes-volumes", + hostName: 'kubernetes-volumes', hostDefinition: {}, serviceDefinition: {}, hostTemplates: [], @@ -36,11 +34,10 @@ export default class Volume extends Resource { /** * kubernetes hosts */ - constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, jsonStream: JSONStream, options: VolumeOptions) { + constructor(logger: Logger, kubeNode: KubeNode, icinga: Icinga, options: VolumeOptions) { super(); this.logger = logger; this.icinga = icinga; - this.jsonStream = jsonStream; this.kubeNode = kubeNode; this.options = Object.assign(this.options, options); } @@ -81,7 +78,7 @@ export default class Volume extends Resource { * Preapre icinga object and apply */ public async prepareObject(definition: any): Promise { - var hostname = this.getHostname(definition); + let hostname = this.getHostname(definition); if (!this.options.attachToNodes) { await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); @@ -117,14 +114,26 @@ export default class Volume extends Resource { * Get hostname */ protected getHostname(definition: any): string { - if(definition.metadata.annotations['kube-icinga/host']) { + if (definition.metadata.annotations['kube-icinga/host']) { return definition.metadata.annotations['kube-icinga/host']; - } else if(this.options.hostName === null) { + } else if (this.options.hostName === null) { return this.escapeName(['volume', definition.metadata.name].join('-')); } return this.options.hostName; - } + } + + /** + * Delete object + */ + protected deleteObject(definition: any): Promise { + if (this.options.hostName === null) { + let hostname = this.getHostname(definition); + return this.icinga.deleteHost(hostname); + } + + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); + } /** * Start kube listener @@ -132,8 +141,7 @@ export default class Volume extends Resource { public async kubeListener(provider) { try { let stream = provider(); - stream.pipe(this.jsonStream); - this.jsonStream.on('data', async (object) => { + stream.on('data', async (object) => { this.logger.debug('received kubernetes persistent volume resource', {object}); if (object.object.kind !== 'PersistentVolume') { @@ -142,8 +150,9 @@ export default class Volume extends Resource { } if (object.type == 'MODIFIED' || object.type == 'DELETED') { - let hostname = this.getHostname(object.object); - await this.icinga.deleteHost(this.options.hostName); + await this.deleteObject(object.object).catch((err) => { + this.logger.error('failed to remove objects', {error: err}) + }); } if (object.type == 'ADDED' || object.type == 'MODIFIED') { @@ -153,7 +162,7 @@ export default class Volume extends Resource { } }); - this.jsonStream.on('finish', () => { + stream.on('finish', () => { this.kubeListener(provider); }); } catch (err) { diff --git a/src/main.ts b/src/main.ts index dacf946..04795a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,36 +10,49 @@ import Volume from './kube/volume'; import * as JSONStream from 'json-stream'; const icinga = new IcingaWrapper(logger, icingaClient); -const kubeNode = new Node(logger, icinga, new JSONStream(), config.kubernetes.nodes); -const kubeIngress = new Ingress(logger, kubeNode, icinga, new JSONStream(), config.kubernetes.ingresses); -const kubeService = new Service(logger, kubeNode, icinga, new JSONStream(), config.kubernetes.services); -const kubeVolume = new Volume(logger, kubeNode, icinga, new JSONStream(), config.kubernetes.volumes); +const kubeNode = new Node(logger, icinga, config.kubernetes.nodes); +const kubeIngress = new Ingress(logger, kubeNode, icinga, config.kubernetes.ingresses); +const kubeService = new Service(logger, kubeNode, icinga, config.kubernetes.services); +const kubeVolume = new Volume(logger, kubeNode, icinga, config.kubernetes.volumes); /** * Main */ async function main() { if (config.cleanup) { - await icinga.cleanup().catch((err) => { - logger.error('failed to cleanup icinga objects', {error: err}); + await icinga.deleteServicesByFilter('service.vars._kubernetes == true').catch((err) => { + logger.error('failed to cleanup icinga services', {error: err}); + }); + + await icinga.deleteHostsByFilter('host.vars._kubernetes == true').catch((err) => { + logger.error('failed to cleanup icinga hosts', {error: err}); }); } if (config.kubernetes.nodes.discover) { kubeNode.kubeListener(function() { - return kubeClient.apis.v1.watch.nodes.getStream(); + let json = new JSONStream(); + let stream = kubeClient.apis.v1.watch.nodes.getStream(); + stream.pipe(json); + return json; }); } if (config.kubernetes.ingresses.discover) { - kubeIngress.kubeListener(function() { - return kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); + kubeIngress.kubeListener(() => { + let json = new JSONStream(); + let stream = kubeClient.apis.extensions.v1beta1.watch.ingresses.getStream(); + stream.pipe(json); + return json; }); } - + if (config.kubernetes.volumes.discover) { kubeVolume.kubeListener(function() { - return kubeClient.apis.v1.watch.persistentvolumes.getStream(); + let json = new JSONStream(); + let stream = kubeClient.apis.v1.watch.persistentvolumes.getStream(); + stream.pipe(json); + return json; }); } @@ -47,7 +60,10 @@ async function main() { || config.kubernetes.services.NodePort.discover || config.kubernetes.services.LoadBalancer.discover) { kubeService.kubeListener(function() { - return kubeClient.apis.v1.watch.services.getStream(); + let json = new JSONStream(); + let stream = kubeClient.apis.v1.watch.services.getStream(); + stream.pipe(json); + return json; }); } } diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index cb3feb5..5e83deb 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -2,9 +2,7 @@ import Ingress from '../../src/kube/ingress'; import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; import {LoggerInstance} from 'winston'; -import * as JSONStream from 'json-stream'; jest.mock('../../src/icinga'); -jest.mock('json-stream'); jest.mock('kubernetes-client'); const template = { @@ -62,7 +60,7 @@ describe('kubernetes ingresses', () => { describe('add ingress object with dummy host', () => { it('create icinga host object', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: false }); Icinga.applyHost = jest.fn(); @@ -74,7 +72,7 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with dynamic host', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: false, hostName: null }); @@ -87,7 +85,7 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with custom definitions', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: false, hostDefinition: { 'vars.foo': 'bar', @@ -103,7 +101,7 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with templates', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: false, hostTemplates: ['foo', 'bar'] }); @@ -115,7 +113,7 @@ describe('kubernetes ingresses', () => { }); it('do not create icinga host object while attachToNodes is enabled', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: false, attachToNodes: true }); @@ -128,7 +126,7 @@ describe('kubernetes ingresses', () => { describe('add ingress object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream); + let instance = new Ingress(LoggerInstance, Node, Icinga); Icinga.applyHost = jest.fn(); Icinga.applyService = jest.fn(); @@ -139,7 +137,7 @@ describe('kubernetes ingresses', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: false }); @@ -152,7 +150,7 @@ describe('kubernetes ingresses', () => { describe('add all ingress object http path rules as service objects', () => { it('create all service objects', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream); + let instance = new Ingress(LoggerInstance, Node, Icinga); Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); @@ -168,7 +166,7 @@ describe('kubernetes ingresses', () => { }); it('create all service objects with dynamic hosts', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { hostName: null }); @@ -182,7 +180,7 @@ describe('kubernetes ingresses', () => { } it('create all service objects with custom service definition', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: true, serviceDefinition: { 'check_command': 'tcp', @@ -201,7 +199,7 @@ describe('kubernetes ingresses', () => { }); it('create all service objects with templates', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -215,7 +213,7 @@ describe('kubernetes ingresses', () => { }); it('create service objects for tls enabled ingresses', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -237,7 +235,7 @@ describe('kubernetes ingresses', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: true }); @@ -256,7 +254,7 @@ describe('kubernetes ingresses', () => { }); it('use annotation instead global definition', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' @@ -274,7 +272,7 @@ describe('kubernetes ingresses', () => { }); it('definiton merge', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Ingress(LoggerInstance, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' diff --git a/tests/kube/node.test.ts b/tests/kube/node.test.ts index 85b9ebd..debc1b0 100644 --- a/tests/kube/node.test.ts +++ b/tests/kube/node.test.ts @@ -69,7 +69,7 @@ describe('kubernetes nodes', () => { }); it('create icinga host object with custom definitions', () => { - let instance = new Node(Logger, Icinga, JSONStream, { + let instance = new Node(Logger, Icinga, { hostDefinition: { 'vars.foo': 'bar', 'vars.check_command': 'foo' @@ -84,7 +84,7 @@ describe('kubernetes nodes', () => { }); it('create icinga host object with templates', () => { - let instance = new Node(Logger, Icinga, JSONStream, { + let instance = new Node(Logger, Icinga, { hostTemplates: ['foo', 'bar'] }); diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts index 6dd926f..105fa4e 100644 --- a/tests/kube/service.test.ts +++ b/tests/kube/service.test.ts @@ -2,10 +2,8 @@ import Service from '../../src/kube/service'; import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; import Logger from '../../src/logger'; -import * as JSONStream from 'json-stream'; jest.mock('../../src/icinga'); jest.mock('../../src/kube/node'); -jest.mock('json-stream'); jest.mock('../../src/logger'); const template = { @@ -52,7 +50,7 @@ beforeEach(() => { describe('kubernetes services', () => { describe('add service object with dummy host', () => { it('create icinga host object', () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: false } @@ -68,7 +66,7 @@ describe('kubernetes services', () => { }); it('create dynamic icinga host object', () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { hostName: null, applyServices: false @@ -85,7 +83,7 @@ describe('kubernetes services', () => { }); it('create icinga host object with custom definitions', () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: false, hostDefinition: { @@ -104,7 +102,7 @@ describe('kubernetes services', () => { }); it('create icinga host object with templates', () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: false, hostTemplates: ['foo', 'bar'] @@ -119,7 +117,7 @@ describe('kubernetes services', () => { }); it('do not create icinga host object while service is of type NodePort', () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { NodePort: { applyServices: false } @@ -134,7 +132,7 @@ describe('kubernetes services', () => { describe('add service object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream); + let instance = new Service(Logger, Node, Icinga); Icinga.applyHost = jest.fn(); Icinga.hasCheckCommand = jest.fn(); @@ -146,7 +144,7 @@ describe('kubernetes services', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { applyServices: false }); @@ -159,7 +157,7 @@ describe('kubernetes services', () => { describe('add all service object ports as service objects', () => { it('create all service objects', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream); + let instance = new Service(Logger, Node, Icinga); Icinga.applyService = jest.fn(); Icinga.applyServiceGroup = jest.fn(); @@ -178,7 +176,7 @@ describe('kubernetes services', () => { }); it('create all service objects with dynamic hosts', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { hostName: null } @@ -194,7 +192,7 @@ describe('kubernetes services', () => { }); it('create all service objects with custom service definition', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -220,7 +218,7 @@ describe('kubernetes services', () => { }); it('create all service objects with custom service definition, check_command not found, fallback to protocol', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -246,7 +244,7 @@ describe('kubernetes services', () => { }); it('create all service objects with templates', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: true, serviceTemplates: ['foo', 'bar'] @@ -265,7 +263,7 @@ describe('kubernetes services', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: true } @@ -289,7 +287,7 @@ describe('kubernetes services', () => { }); it('use annotation instead global definition', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: true, serviceDefinition: { @@ -312,7 +310,7 @@ describe('kubernetes services', () => { }); it('definiton merge', async () => { - let instance = new Service(Logger, Node, Icinga, JSONStream, { + let instance = new Service(Logger, Node, Icinga, { ClusterIP: { applyServices: true, serviceDefinition: { diff --git a/tests/kube/volume.test.ts b/tests/kube/volume.test.ts index 2dff1be..107163f 100644 --- a/tests/kube/volume.test.ts +++ b/tests/kube/volume.test.ts @@ -2,9 +2,7 @@ import Volume from '../../src/kube/volume'; import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; import {LoggerInstance} from 'winston'; -import * as JSONStream from 'json-stream'; jest.mock('../../src/icinga'); -jest.mock('json-stream'); jest.mock('kubernetes-client'); const template = { @@ -57,7 +55,7 @@ describe('kubernetes volumes', () => { describe('add volume object with dummy host', () => { it('create icinga host object', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: false }); Icinga.applyHost = jest.fn(); @@ -69,7 +67,7 @@ describe('kubernetes volumes', () => { }); it('create icinga host object with dynamic host', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: false, hostName: null }); @@ -82,7 +80,7 @@ describe('kubernetes volumes', () => { }); it('create icinga host object with custom definitions', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: false, hostDefinition: { 'vars.foo': 'bar', @@ -98,7 +96,7 @@ describe('kubernetes volumes', () => { }); it('create icinga host object with templates', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: false, hostTemplates: ['foo', 'bar'] }); @@ -110,7 +108,7 @@ describe('kubernetes volumes', () => { }); it('do not create icinga host object while attachToNodes is enabled', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: false, attachToNodes: true }); @@ -123,7 +121,7 @@ describe('kubernetes volumes', () => { describe('add volume object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream); + let instance = new Volume(LoggerInstance, Node, Icinga); Icinga.applyHost = jest.fn(); Icinga.applyService = jest.fn(); @@ -134,7 +132,7 @@ describe('kubernetes volumes', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: false }); @@ -147,7 +145,7 @@ describe('kubernetes volumes', () => { describe('add all volume object http path rules as service objects', () => { it('create service object', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream); + let instance = new Volume(LoggerInstance, Node, Icinga); Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); @@ -159,7 +157,7 @@ describe('kubernetes volumes', () => { }); it('create service object with dynamic host', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { hostName: null }); @@ -172,7 +170,7 @@ describe('kubernetes volumes', () => { } it('create all service objects with custom service definition', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: true, serviceDefinition: { 'check_command': 'tcp', @@ -189,7 +187,7 @@ describe('kubernetes volumes', () => { }); it('create all service objects with templates', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -204,7 +202,7 @@ describe('kubernetes volumes', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: true }); @@ -221,7 +219,7 @@ describe('kubernetes volumes', () => { }); it('use annotation instead global definition', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' @@ -238,7 +236,7 @@ describe('kubernetes volumes', () => { }); it('definiton merge', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, JSONStream, { + let instance = new Volume(LoggerInstance, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' From b039054c40e6677251bdbc1c8902ca67c0221230 Mon Sep 17 00:00:00 2001 From: raffis Date: Fri, 15 Feb 2019 15:30:25 +0100 Subject: [PATCH 11/15] readme fixes --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b70c5e..b5906e9 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,14 @@ Multiple watchers are bootstraped and listen for any kubernetes changes. Those c * [Ingresses](#ingresses) * [Services](#services) * [Volumes](#volumes) -* [Requirement](#requirements) +* [Requirements](#requirements) * [Setup icinga2 api user](#setup-icinga2-api-user) * [Deployment](#deployment) +* [Resource visibility](#resource-visibility) * [Advanced topics](#advanced-topics) - * [ClusterIP services](#clusterip-services) * [Using icinga2 apply rules](#using-icinga2-apply-rules) - * [Overwite specific icinga object definition](#overwite-specific-icinga-object-definition) + * [Globally overwrite icinga object definitions](#globally-overwrite-icinga-object-definitions) + * [Overwrite icinga object definitions directly in kubernetes resources](overwrite-icinga-object-definitions-directly-in-kubernetes-resources) * [Configuration](#configuration) ## Resource types From b68cd500c537e26fb0761ec79f94c98518737097 Mon Sep 17 00:00:00 2001 From: raffis Date: Mon, 18 Feb 2019 14:50:27 +0100 Subject: [PATCH 12/15] added new tests --- package.json | 2 +- src/icinga.ts | 38 ---------- src/kube/ingress.ts | 7 +- tests/icinga.test.ts | 10 --- tests/kube/ingress.test.ts | 124 ++++++++++++++++++++++++++++----- tests/kube/node.test.ts | 84 +++++++++++++++++++---- tests/kube/service.test.ts | 137 ++++++++++++++++++++++++++++++++++++- tests/kube/volume.test.ts | 122 ++++++++++++++++++++++++++++----- 8 files changed, 425 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index 8ab21da..8395fe0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kube-icinga", - "version": "1.0.0", + "version": "2.0.0", "description": "Icinga2 autodiscovery service for kubernetes", "main": "main.js", "scripts": { diff --git a/src/icinga.ts b/src/icinga.ts index e3793e2..96d03dc 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -207,50 +207,12 @@ export default class Icinga { }); } - /** - * Cleanup all previously deployed icinga kubernetes objects - */ - /*public async cleanup(): Promise { - this.logger.info('start cleanup, removing all kubernetes objects from icinga'); - - return new Promise((resolve, reject) => { - this.icingaClient.getServiceFiltered({filter: 'service.vars._kubernetes == true'}, async (err, result) => { - if (err) { - return reject(err); - } - - let handlers = []; - for (const service of result) { - handlers.push(this.deleteService(service.attrs.host_name, service.attrs.name)); - } - - await Promise.all(handlers); - resolve(true); - }); - - this.icingaClient.getHostFiltered({filter: 'host.vars._kubernetes == true'}, async (err, result) => { - if (err) { - return reject(err); - } - - let handlers = []; - for (const host of result) { - handlers.push(this.deleteHost(host.attrs.name)); - } - - await Promise.all(handlers); - resolve(true); - }); - }); - }*/ - /** * Delete services by filter */ public async deleteServicesByFilter(filter: string): Promise { return new Promise((resolve, reject) => { this.icingaClient.getServiceFiltered({filter: filter}, async (err, result) => { -console.log(err, result); if (err) { return reject(err); } diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index f2308a9..32dac54 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -83,11 +83,10 @@ export default class Ingress extends Resource { if (!this.options.attachToNodes) { await this.applyHost(hostname, hostname, definition, this.options.hostTemplates); } - let service = this.prepareResource(definition); let templates = this.options.serviceTemplates; templates = templates.concat(this.prepareTemplates(definition)); - + if (this.options.applyServices) { await this.icinga.applyServiceGroup(definition.metadata.namespace); @@ -145,7 +144,7 @@ export default class Ingress extends Resource { let hostname = this.getHostname(definition); return this.icinga.deleteHost(hostname); } - + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); } @@ -167,7 +166,7 @@ export default class Ingress extends Resource { await this.deleteObject(object.object).catch((err) => { this.logger.error('failed to remove objects', {error: err}) }); - } + } if (object.type == 'ADDED' || object.type == 'MODIFIED') { this.prepareObject(object.object).catch((err) => { diff --git a/tests/icinga.test.ts b/tests/icinga.test.ts index dd23e42..08a7601 100644 --- a/tests/icinga.test.ts +++ b/tests/icinga.test.ts @@ -1,13 +1,3 @@ -/* - public hasCheckCommand(command: string): Promise { - public applyHostGroup(name: string): Promise { - public applyServiceGroup(name: string): Promise { - public applyHost(name: string, address: string, definition, templates: string[]=[]): Promise { - public applyService(host: string, name: string, definition, templates: string[]=[]) { - public deleteService(host: string, name: string): Promise { - public deleteHost(name: string): Promise { - public async cleanup(): Promise { -*/ import * as IcingaApi from 'icinga2-api'; import IcingaClient from '../src/icinga'; import Logger from '../src/logger'; diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index 5e83deb..c9c04ad 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -1,9 +1,10 @@ import Ingress from '../../src/kube/ingress'; import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; -import {LoggerInstance} from 'winston'; +import Logger from '../../src/logger'; jest.mock('../../src/icinga'); jest.mock('kubernetes-client'); +jest.mock('../../src/logger'); const template = { "apiVersion": "extensions/v1beta1", @@ -12,6 +13,7 @@ const template = { "annotations": {}, "name": "foo", "namespace": "foobar", + "uid": "xyz" }, "spec": { "rules": [ @@ -56,11 +58,99 @@ beforeEach(() => { }); describe('kubernetes ingresses', () => { - var instance: Ingress; + describe('ingress watch stream', () => { + it('create icinga ingress object', async () => { + let instance = new Ingress(Logger, Node, Icinga); + var resource = { + type: 'ADDED', + object: fixture + }; + + Icinga.deleteServicesByFilter = jest.fn(); + Icinga.applyHost = jest.fn(); + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + expect(Icinga.deleteServicesByFilter.mock.calls.length).toBe(0); + }); + + it('modify ingress object delete and create', async () => { + let instance = new Ingress(Logger, Node, Icinga); + var resource = { + type: 'MODIFIED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = function(definition) { + expect(definition).toEqual('service.vars.kubernetes.metadata.uid==\"xyz\"'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + var bindings = {}; + var json = { + on: async function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + }); + + it('delete ingress object delete', async () => { + let instance = new Ingress(Logger, Node, Icinga); + var resource = { + type: 'DELETED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = function(definition) { + expect(definition).toEqual('service.vars.kubernetes.metadata.uid==\"xyz\"'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback.bind(instance); + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(0); + }); + }); + describe('add ingress object with dummy host', () => { it('create icinga host object', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: false }); Icinga.applyHost = jest.fn(); @@ -72,7 +162,7 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with dynamic host', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: false, hostName: null }); @@ -85,7 +175,7 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with custom definitions', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: false, hostDefinition: { 'vars.foo': 'bar', @@ -101,7 +191,7 @@ describe('kubernetes ingresses', () => { }); it('create icinga host object with templates', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: false, hostTemplates: ['foo', 'bar'] }); @@ -113,7 +203,7 @@ describe('kubernetes ingresses', () => { }); it('do not create icinga host object while attachToNodes is enabled', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: false, attachToNodes: true }); @@ -126,7 +216,7 @@ describe('kubernetes ingresses', () => { describe('add ingress object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga); + let instance = new Ingress(Logger, Node, Icinga); Icinga.applyHost = jest.fn(); Icinga.applyService = jest.fn(); @@ -137,7 +227,7 @@ describe('kubernetes ingresses', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: false }); @@ -150,7 +240,7 @@ describe('kubernetes ingresses', () => { describe('add all ingress object http path rules as service objects', () => { it('create all service objects', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga); + let instance = new Ingress(Logger, Node, Icinga); Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); @@ -166,7 +256,7 @@ describe('kubernetes ingresses', () => { }); it('create all service objects with dynamic hosts', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { hostName: null }); @@ -180,7 +270,7 @@ describe('kubernetes ingresses', () => { } it('create all service objects with custom service definition', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: true, serviceDefinition: { 'check_command': 'tcp', @@ -199,7 +289,7 @@ describe('kubernetes ingresses', () => { }); it('create all service objects with templates', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -213,7 +303,7 @@ describe('kubernetes ingresses', () => { }); it('create service objects for tls enabled ingresses', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -235,7 +325,7 @@ describe('kubernetes ingresses', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: true }); @@ -254,7 +344,7 @@ describe('kubernetes ingresses', () => { }); it('use annotation instead global definition', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' @@ -272,7 +362,7 @@ describe('kubernetes ingresses', () => { }); it('definiton merge', async () => { - let instance = new Ingress(LoggerInstance, Node, Icinga, { + let instance = new Ingress(Logger, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' diff --git a/tests/kube/node.test.ts b/tests/kube/node.test.ts index debc1b0..7b5d08f 100644 --- a/tests/kube/node.test.ts +++ b/tests/kube/node.test.ts @@ -26,22 +26,82 @@ beforeEach(() => { describe('kubernetes nodes', () => { describe('nodes watch stream', () => { - it('create icinga host object', () => { - let instance = new Node(Logger, Icinga, new JSONStream()); - instance.prepareObject = jest.fn(); - /*Icinga.applyHost = jest.fn(); - instance.prepareObject(fixture); - const call = Icinga.applyHost.mock.calls[0];*/ - var stream = new Readable(); - stream._read = () => {}; - stream.push(JSON.stringify([{ + it('create icinga host object', async () => { + let instance = new Node(Logger, Icinga); + var resource = { + type: 'ADDED', + object: fixture + }; + + instance.prepareObject = function(definition) { + expect(definition).toEqual(resource.object); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + bindings.data(resource); + }); + + it('modify host object no action', async () => { + let instance = new Node(Logger, Icinga); + var resource = { type: 'MODIFIED', object: fixture - }])); + }; + + instance.prepareObject = jest.fn(); - instance.kubeListener(() => { - return stream; + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + bindings.data(resource); + expect(instance.prepareObject.mock.calls.length).toBe(0); + }); + + it('delete host object delete', async () => { + let instance = new Node(Logger, Icinga); + var resource = { + type: 'DELETED', + object: fixture + }; + + Icinga.deleteHost = jest.fn(); + instance.prepareObject = jest.fn(); + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; }); + + bindings.data(resource); + expect(Icinga.deleteHost.mock.calls.length).toEqual(1) + expect(instance.prepareObject.mock.calls.length).toEqual(0); }); }); diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts index 105fa4e..7513bbd 100644 --- a/tests/kube/service.test.ts +++ b/tests/kube/service.test.ts @@ -12,7 +12,8 @@ const template = { "metadata": { "name": "foo", "namespace": "foobar", - "annotations": {} + "annotations": {}, + "uid": "xyz" }, "spec": { "clusterIP": "10.99.24.32", @@ -48,6 +49,140 @@ beforeEach(() => { }); describe('kubernetes services', () => { + describe('service watch stream', () => { + it('do not create icinga service object if typ is disabled for provisioning', async () => { + let instance = new Service(Logger, Node, Icinga, { + ClusterIP: { + discover: false + } + }); + + var resource = { + type: 'ADDED', + object: fixture + }; + + Icinga.deleteServicesByFilter = jest.fn(); + Icinga.applyHost = jest.fn(); + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(0); + expect(Icinga.deleteServicesByFilter.mock.calls.length).toBe(0); + }); + + it('create icinga service object', async () => { + let instance = new Service(Logger, Node, Icinga, { + ClusterIP: { + discover: true + } + }); + var resource = { + type: 'ADDED', + object: fixture + }; + + Icinga.deleteServicesByFilter = jest.fn(); + Icinga.applyHost = jest.fn(); + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + expect(Icinga.deleteServicesByFilter.mock.calls.length).toBe(0); + }); + + it('modify service object delete and create', async () => { + let instance = new Service(Logger, Node, Icinga, { + ClusterIP: { + discover: true + } + }); + + var resource = { + type: 'MODIFIED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = function(definition) { + expect(definition).toEqual('service.vars.kubernetes.metadata.uid==\"xyz\"'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + var bindings = {}; + var json = { + on: async function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + }); + + it('delete service object delete', async () => { + let instance = new Service(Logger, Node, Icinga, { + ClusterIP: { + discover: true + } + }); + + var resource = { + type: 'DELETED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = function(definition) { + expect(definition).toEqual('service.vars.kubernetes.metadata.uid==\"xyz\"'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback.bind(instance); + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(0); + }); + }); + describe('add service object with dummy host', () => { it('create icinga host object', () => { let instance = new Service(Logger, Node, Icinga, { diff --git a/tests/kube/volume.test.ts b/tests/kube/volume.test.ts index 107163f..d28cd26 100644 --- a/tests/kube/volume.test.ts +++ b/tests/kube/volume.test.ts @@ -1,7 +1,8 @@ import Volume from '../../src/kube/volume'; import Node from '../../src/kube/node'; import Icinga from '../../src/icinga'; -import {LoggerInstance} from 'winston'; +import Logger from '../../src/logger'; +jest.mock('../../src/logger'); jest.mock('../../src/icinga'); jest.mock('kubernetes-client'); @@ -15,6 +16,7 @@ const template = { "volume.beta.kubernetes.io/storage-class": "generic-nimble" }, "name": "generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2", + "uid": "xyz" }, "spec": { "accessModes": [ @@ -51,11 +53,99 @@ beforeEach(() => { }); describe('kubernetes volumes', () => { - var instance: Volume; + describe('volume watch stream', () => { + it('create icinga volume object', async () => { + let instance = new Volume(Logger, Node, Icinga); + var resource = { + type: 'ADDED', + object: fixture + }; + + Icinga.deleteServicesByFilter = jest.fn(); + Icinga.applyHost = jest.fn(); + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + expect(Icinga.deleteServicesByFilter.mock.calls.length).toBe(0); + }); + + it('modify volume object delete and create', async () => { + let instance = new Volume(Logger, Node, Icinga); + var resource = { + type: 'MODIFIED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = function(definition) { + expect(definition).toEqual('service.vars.kubernetes.metadata.uid==\"xyz\"'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + var bindings = {}; + var json = { + on: async function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + }); + + it('delete volume object delete', async () => { + let instance = new Volume(Logger, Node, Icinga); + + var resource = { + type: 'DELETED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = function(definition) { + expect(definition).toEqual('service.vars.kubernetes.metadata.uid==\"xyz\"'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + + var bindings = {}; + var json = { + on: function(name, callback) { + bindings[name] = callback.bind(instance); + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(0); + }); + }); describe('add volume object with dummy host', () => { it('create icinga host object', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: false }); Icinga.applyHost = jest.fn(); @@ -67,7 +157,7 @@ describe('kubernetes volumes', () => { }); it('create icinga host object with dynamic host', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: false, hostName: null }); @@ -80,7 +170,7 @@ describe('kubernetes volumes', () => { }); it('create icinga host object with custom definitions', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: false, hostDefinition: { 'vars.foo': 'bar', @@ -96,7 +186,7 @@ describe('kubernetes volumes', () => { }); it('create icinga host object with templates', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: false, hostTemplates: ['foo', 'bar'] }); @@ -108,7 +198,7 @@ describe('kubernetes volumes', () => { }); it('do not create icinga host object while attachToNodes is enabled', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: false, attachToNodes: true }); @@ -121,7 +211,7 @@ describe('kubernetes volumes', () => { describe('add volume object namespace as service group', () => { it('create service group per default', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga); + let instance = new Volume(Logger, Node, Icinga); Icinga.applyHost = jest.fn(); Icinga.applyService = jest.fn(); @@ -132,7 +222,7 @@ describe('kubernetes volumes', () => { }); it('do not create servicegroup if applyServices is disabled', () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: false }); @@ -145,7 +235,7 @@ describe('kubernetes volumes', () => { describe('add all volume object http path rules as service objects', () => { it('create service object', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga); + let instance = new Volume(Logger, Node, Icinga); Icinga.applyServiceGroup = jest.fn(); Icinga.applyService = jest.fn(); @@ -157,7 +247,7 @@ describe('kubernetes volumes', () => { }); it('create service object with dynamic host', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { hostName: null }); @@ -170,7 +260,7 @@ describe('kubernetes volumes', () => { } it('create all service objects with custom service definition', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: true, serviceDefinition: { 'check_command': 'tcp', @@ -187,7 +277,7 @@ describe('kubernetes volumes', () => { }); it('create all service objects with templates', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: true, serviceTemplates: ['foo', 'bar'] }); @@ -202,7 +292,7 @@ describe('kubernetes volumes', () => { describe('kubernetes annotations', () => { it('check_command/templates annotation', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: true }); @@ -219,7 +309,7 @@ describe('kubernetes volumes', () => { }); it('use annotation instead global definition', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' @@ -236,7 +326,7 @@ describe('kubernetes volumes', () => { }); it('definiton merge', async () => { - let instance = new Volume(LoggerInstance, Node, Icinga, { + let instance = new Volume(Logger, Node, Icinga, { applyServices: true, serviceDefinition: { check_command: 'foo' From fed30ffad7439230bb0d6addc2703e2fd7b14812 Mon Sep 17 00:00:00 2001 From: raffis Date: Tue, 19 Feb 2019 13:53:54 +0100 Subject: [PATCH 13/15] v2.0.0 fixes --- src/kube/ingress.ts | 12 ++++++------ src/kube/service.ts | 6 +++--- src/kube/volume.ts | 9 +++++---- src/main.ts | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/kube/ingress.ts b/src/kube/ingress.ts index 32dac54..c2786f9 100644 --- a/src/kube/ingress.ts +++ b/src/kube/ingress.ts @@ -86,7 +86,7 @@ export default class Ingress extends Resource { let service = this.prepareResource(definition); let templates = this.options.serviceTemplates; templates = templates.concat(this.prepareTemplates(definition)); - + if (this.options.applyServices) { await this.icinga.applyServiceGroup(definition.metadata.namespace); @@ -138,14 +138,14 @@ export default class Ingress extends Resource { /** * Delete object - */ + */ protected deleteObject(definition: any): Promise { if (this.options.hostName === null) { let hostname = this.getHostname(definition); return this.icinga.deleteHost(hostname); } - - return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); + + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); } /** @@ -164,9 +164,9 @@ export default class Ingress extends Resource { if (object.type == 'MODIFIED' || object.type == 'DELETED') { await this.deleteObject(object.object).catch((err) => { - this.logger.error('failed to remove objects', {error: err}) + this.logger.error('failed to remove objects', {error: err}); }); - } + } if (object.type == 'ADDED' || object.type == 'MODIFIED') { this.prepareObject(object.object).catch((err) => { diff --git a/src/kube/service.ts b/src/kube/service.ts index d0cda80..3ebf5b3 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -183,7 +183,7 @@ export default class Service extends Resource { /** * Delete object - */ + */ protected deleteObject(definition: any): Promise { let serviceType = definition.spec.type; @@ -192,7 +192,7 @@ export default class Service extends Resource { return this.icinga.deleteHost(hostname); } - return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); } /** @@ -216,7 +216,7 @@ export default class Service extends Resource { if (object.type == 'MODIFIED' || object.type == 'DELETED') { await this.deleteObject(object.object).catch((err) => { - this.logger.error('failed to remove objects', {error: err}) + this.logger.error('failed to remove objects', {error: err}); }); } diff --git a/src/kube/volume.ts b/src/kube/volume.ts index f1d64ea..fb500d4 100644 --- a/src/kube/volume.ts +++ b/src/kube/volume.ts @@ -86,6 +86,7 @@ export default class Volume extends Resource { if (this.options.applyServices) { let groups = []; + if (definition.spec.claimRef.namespace) { groups.push(definition.spec.claimRef.namespace); await this.icinga.applyServiceGroup(definition.spec.claimRef.namespace); @@ -125,15 +126,15 @@ export default class Volume extends Resource { /** * Delete object - */ + */ protected deleteObject(definition: any): Promise { if (this.options.hostName === null) { let hostname = this.getHostname(definition); return this.icinga.deleteHost(hostname); } - return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); - } + return this.icinga.deleteServicesByFilter('service.vars.kubernetes.metadata.uid=="'+definition.metadata.uid+'"'); + } /** * Start kube listener @@ -151,7 +152,7 @@ export default class Volume extends Resource { if (object.type == 'MODIFIED' || object.type == 'DELETED') { await this.deleteObject(object.object).catch((err) => { - this.logger.error('failed to remove objects', {error: err}) + this.logger.error('failed to remove objects', {error: err}); }); } diff --git a/src/main.ts b/src/main.ts index 04795a3..4241c0d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,7 +23,7 @@ async function main() { await icinga.deleteServicesByFilter('service.vars._kubernetes == true').catch((err) => { logger.error('failed to cleanup icinga services', {error: err}); }); - + await icinga.deleteHostsByFilter('host.vars._kubernetes == true').catch((err) => { logger.error('failed to cleanup icinga hosts', {error: err}); }); @@ -46,7 +46,7 @@ async function main() { return json; }); } - + if (config.kubernetes.volumes.discover) { kubeVolume.kubeListener(function() { let json = new JSONStream(); From 2644785e310db5e9d561b8566e66d06b8e4bac6a Mon Sep 17 00:00:00 2001 From: raffis Date: Wed, 20 Feb 2019 14:49:19 +0100 Subject: [PATCH 14/15] v2.0.0 fixes --- CHANGELOG.md | 2 + config.json | 2 + src/client/icinga.ts | 2 +- src/config.ts | 92 +++++++++++++++++++------------------- src/icinga.ts | 27 +++++++++++ src/kube/service.ts | 10 +++++ src/logger.ts | 2 +- src/main.ts | 6 +-- tests/icinga.test.ts | 37 ++++++++++++++- tests/kube/service.test.ts | 50 +++++++++++++++++++++ 10 files changed, 177 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f6c07..51bac79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ * [FIX] fixed Error: Invalid attribute specified: host_name\n for kube nodes * [FIX] fixed duplicate ingress objects (different path, same ingress names in different namespaces, ...) * [CHANGE] Added hostName setting, by default all resources (except nodes) get attached to single host object of their type (can be configured differently) +* [CHANGE] Implemented workaround for icinga issue https://github.com/Icinga/icinga2/issues/6012, restart icinga service after adding new objects +* [FIX] Changing boolean values in config.json now reflects as configured (Set to false was not possible in v1.x) ## 1.0.1 diff --git a/config.json b/config.json index 3d2637c..8e4e781 100644 --- a/config.json +++ b/config.json @@ -28,6 +28,7 @@ "volumes": { "discover": true, "hostName": "kubernetes-volumes", + "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], @@ -38,6 +39,7 @@ "ClusterIP": { "discover": false, "hostName": "kubernetes-clusterip-services", + "serviceDefinition": {}, "hostDefinition": {}, "serviceTemplates": ["generic-service"], "hostTemplates": ["generic-host"], diff --git a/src/client/icinga.ts b/src/client/icinga.ts index 1c0782e..134e9de 100644 --- a/src/client/icinga.ts +++ b/src/client/icinga.ts @@ -1,5 +1,5 @@ import * as IcingaApi from 'icinga2-api'; import config from '../config'; -const icingaClient = new IcingaApi(config.icinga.address, config.icinga.port, config.icinga.apiUser, config.icinga.apiPassword); +const icingaClient = new IcingaApi(config.icinga.address || '127.0.0.1', config.icinga.port || 5661, config.icinga.apiUser || 'admin', config.icinga.apiPassword || 'admin'); export default icingaClient; diff --git a/src/config.ts b/src/config.ts index cd9164e..41a5a94 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,68 +9,68 @@ try { const config = { log: { - level: process.env.LOG_LEVEL || defaultConfig.log.level || 'info', + level: process.env.LOG_LEVEL || defaultConfig.log.level, }, - cleanup: process.env.CLEANUP || defaultConfig.cleanup || true, + cleanup: process.env.CLEANUP || defaultConfig.cleanup, icinga: { - address: process.env.ICINGA_ADDRESS || defaultConfig.icinga.address || '127.0.0.1', - port: process.env.ICINGA_PORT || defaultConfig.icinga.port || '5661', - apiUser: process.env.ICINGA_API_USERNAME || defaultConfig.icinga.apiUser || 'admin', - apiPassword: process.env.ICINGA_API_PASSWORD || defaultConfig.icinga.apiPassword || 'admin', + address: process.env.ICINGA_ADDRESS || defaultConfig.icinga.address, + port: process.env.ICINGA_PORT || defaultConfig.icinga.port, + apiUser: process.env.ICINGA_API_USERNAME || defaultConfig.icinga.apiUser, + apiPassword: process.env.ICINGA_API_PASSWORD || defaultConfig.icinga.apiPassword, }, kubernetes: { nodes: { - discover: process.env.KUBERNETES_NODES_DISCOVER || defaultConfig.kubernetes.nodes.discover || true, - hostDefinition: process.env.KUBERNETES_NODES_HOST_DEFINITION || defaultConfig.kubernetes.nodes.hostDefinition || {}, - hostTemplates: process.env.KUBERNETES_NODES_HOST_TEMPLATES || defaultConfig.kubernetes.nodes.hostTemplates || ['generic-host'], + discover: process.env.KUBERNETES_NODES_DISCOVER || defaultConfig.kubernetes.nodes.discover, + hostDefinition: process.env.KUBERNETES_NODES_HOST_DEFINITION || defaultConfig.kubernetes.nodes.hostDefinition, + hostTemplates: process.env.KUBERNETES_NODES_HOST_TEMPLATES || defaultConfig.kubernetes.nodes.hostTemplates, }, ingresses: { - discover: process.env.KUBERNETES_INGRESSES_DISCOVER || defaultConfig.kubernetes.ingresses.discover || true, - hostName: process.env.KUBERNETES_INGRESSES_HOSTNAME || defaultConfig.kubernetes.ingresses.hostName || 'kubernetes-ingresses', - applyServices: process.env.KUBERNETES_INGRESSES_APPLYSERVICES || defaultConfig.kubernetes.ingresses.applyServices || true, - serviceDefinition: process.env.KUBERNETES_INGRESSES_SERVICE_DEFINITION || defaultConfig.kubernetes.ingresses.serviceDefinition || {}, - hostDefinition: process.env.KUBERNETES_INGRESSES_HOST_DEFINITION || defaultConfig.kubernetes.ingresses.hostDefinition || {}, - serviceTemplates: process.env.KUBERNETES_INGRESSES_SERVICE_TEMPLATES || defaultConfig.kubernetes.ingresses.serviceTemplates || ['generic-service'], - hostTemplates: process.env.KUBERNETES_INGRESSES_HOST_TEMPLATES || defaultConfig.kubernetes.ingresses.hostTemplates || ['generic-host'], - attachToNodes: process.env.KUBERNETES_INGRESSES_ATTACHTONODES || defaultConfig.kubernetes.ingresses.attachToNodes || false, + discover: process.env.KUBERNETES_INGRESSES_DISCOVER || defaultConfig.kubernetes.ingresses.discover, + hostName: process.env.KUBERNETES_INGRESSES_HOSTNAME || defaultConfig.kubernetes.ingresses.hostName, + applyServices: process.env.KUBERNETES_INGRESSES_APPLYSERVICES || defaultConfig.kubernetes.ingresses.applyServices, + serviceDefinition: process.env.KUBERNETES_INGRESSES_SERVICE_DEFINITION || defaultConfig.kubernetes.ingresses.serviceDefinition, + hostDefinition: process.env.KUBERNETES_INGRESSES_HOST_DEFINITION || defaultConfig.kubernetes.ingresses.hostDefinition, + serviceTemplates: process.env.KUBERNETES_INGRESSES_SERVICE_TEMPLATES || defaultConfig.kubernetes.ingresses.serviceTemplates, + hostTemplates: process.env.KUBERNETES_INGRESSES_HOST_TEMPLATES || defaultConfig.kubernetes.ingresses.hostTemplates, + attachToNodes: process.env.KUBERNETES_INGRESSES_ATTACHTONODES || defaultConfig.kubernetes.ingresses.attachToNodes, }, volumes: { - discover: process.env.KUBERNETES_VOLUMES_DISCOVER || defaultConfig.kubernetes.volumes.discover || true, - hostName: process.env.KUBERNETES_VOLUMES_HOSTNAME || defaultConfig.kubernetes.volumes.hostName || 'kubernetes-volumes', - applyServices: process.env.KUBERNETES_VOLUMES_APPLYSERVICES || defaultConfig.kubernetes.volumes.applyServices || true, - serviceDefinition: process.env.KUBERNETES_VOLUMES_SERVICE_DEFINITION || defaultConfig.kubernetes.volumes.serviceDefinition || {}, - hostDefinition: process.env.KUBERNETES_VOLUMES_HOST_DEFINITION || defaultConfig.kubernetes.volumes.hostDefinition || {}, - serviceTemplates: process.env.KUBERNETES_VOLUMES_SERVICE_TEMPLATES || defaultConfig.kubernetes.volumes.serviceTemplates || ['generic-service'], - hostTemplates: process.env.KUBERNETES_VOLUMES_HOST_TEMPLATES || defaultConfig.kubernetes.volumes.hostTemplates || ['generic-host'], - attachToNodes: process.env.KUBERNETES_VOLUMES_ATTACHTONODES || defaultConfig.kubernetes.volumes.attachToNodes || false, + discover: process.env.KUBERNETES_VOLUMES_DISCOVER || defaultConfig.kubernetes.volumes.discover, + hostName: process.env.KUBERNETES_VOLUMES_HOSTNAME || defaultConfig.kubernetes.volumes.hostName, + applyServices: process.env.KUBERNETES_VOLUMES_APPLYSERVICES || defaultConfig.kubernetes.volumes.applyServices, + serviceDefinition: process.env.KUBERNETES_VOLUMES_SERVICE_DEFINITION || defaultConfig.kubernetes.volumes.serviceDefinition, + hostDefinition: process.env.KUBERNETES_VOLUMES_HOST_DEFINITION || defaultConfig.kubernetes.volumes.hostDefinition, + serviceTemplates: process.env.KUBERNETES_VOLUMES_SERVICE_TEMPLATES || defaultConfig.kubernetes.volumes.serviceTemplates, + hostTemplates: process.env.KUBERNETES_VOLUMES_HOST_TEMPLATES || defaultConfig.kubernetes.volumes.hostTemplates, + attachToNodes: process.env.KUBERNETES_VOLUMES_ATTACHTONODES || defaultConfig.kubernetes.volumes.attachToNodes, }, services: { ClusterIP: { - discover: process.env.KUBERNETES_SERVICES_CLUSTERIP_DISCOVER || defaultConfig.kubernetes.services.ClusterIP.discover || false, - hostName: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOSTNAME || defaultConfig.kubernetes.services.ClusterIP.hostName || 'kubernetes-clusterip-services', - applyServices: process.env.KUBERNETES_SERVICES_CLUSTERIP_APPLYSERVICES || defaultConfig.kubernetes.services.ClusterIP.applyServices || true, - serviceDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_SERVICE_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.serviceDefinition || {}, - hostDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.hostDefinition || {}, - serviceTemplates: process.env.KUBERNETES_SERVICES_CLUSTERIP_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.ClusterIP.serviceTemplates || ['generic-service'], - hostTemplates: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_TEMPLATES || defaultConfig.kubernetes.services.ClusterIP.hostTemplates || ['generic-host'], + discover: process.env.KUBERNETES_SERVICES_CLUSTERIP_DISCOVER || defaultConfig.kubernetes.services.ClusterIP.discover, + hostName: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOSTNAME || defaultConfig.kubernetes.services.ClusterIP.hostName, + applyServices: process.env.KUBERNETES_SERVICES_CLUSTERIP_APPLYSERVICES || defaultConfig.kubernetes.services.ClusterIP.applyServices, + serviceDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_SERVICE_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.serviceDefinition, + hostDefinition: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_DEFINITION || defaultConfig.kubernetes.services.ClusterIP.hostDefinition, + serviceTemplates: process.env.KUBERNETES_SERVICES_CLUSTERIP_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.ClusterIP.serviceTemplates, + hostTemplates: process.env.KUBERNETES_SERVICES_CLUSTERIP_HOST_TEMPLATES || defaultConfig.kubernetes.services.ClusterIP.hostTemplates, }, NodePort: { - discover: process.env.KUBERNETES_SERVICES_NODEPORT_DISCOVER || defaultConfig.kubernetes.services.NodePort.discover || true, - hostName: process.env.KUBERNETES_SERVICES_NODEPORT_HOSTNAME || defaultConfig.kubernetes.services.NodePort.hostName || 'kubernetes-nodeport-services', - applyServices: process.env.KUBERNETES_SERVICES_NODEPORT_APPLYSERVICES || defaultConfig.kubernetes.services.NodePort.applyServices || true, - serviceDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_SERVICE_DEFINITION || defaultConfig.kubernetes.services.NodePort.serviceDefinition || {}, - hostDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_DEFINITION || defaultConfig.kubernetes.services.NodePort.hostDefinition || {}, - serviceTemplates: process.env.KUBERNETES_SERVICES_NODEPORT_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.NodePort.serviceTemplates || ['generic-service'], - hostTemplates: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_TEMPLATES || defaultConfig.kubernetes.services.NodePort.hostTemplates || ['generic-host'], + discover: process.env.KUBERNETES_SERVICES_NODEPORT_DISCOVER || defaultConfig.kubernetes.services.NodePort.discover, + hostName: process.env.KUBERNETES_SERVICES_NODEPORT_HOSTNAME || defaultConfig.kubernetes.services.NodePort.hostName, + applyServices: process.env.KUBERNETES_SERVICES_NODEPORT_APPLYSERVICES || defaultConfig.kubernetes.services.NodePort.applyServices, + serviceDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_SERVICE_DEFINITION || defaultConfig.kubernetes.services.NodePort.serviceDefinition, + hostDefinition: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_DEFINITION || defaultConfig.kubernetes.services.NodePort.hostDefinition, + serviceTemplates: process.env.KUBERNETES_SERVICES_NODEPORT_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.NodePort.serviceTemplates, + hostTemplates: process.env.KUBERNETES_SERVICES_NODEPORT_HOST_TEMPLATES || defaultConfig.kubernetes.services.NodePort.hostTemplates, }, LoadBalancer: { - discover: process.env.KUBERNETES_SERVICES_LOADBALANCER_DISCOVER || defaultConfig.kubernetes.services.LoadBalancer.discover || true, - hostName: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOSTNAME || defaultConfig.kubernetes.services.LoadBalancer.hostName || 'kubernetes-loadbalancer-services', - applyServices: process.env.KUBERNETES_SERVICES_LOADBALANCER_APPLYSERVICES || defaultConfig.kubernetes.services.LoadBalancer.applyServices || true, - serviceDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_SERVICE_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.serviceDefinition || {}, - hostDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.hostDefinition || {}, - serviceTemplates: process.env.KUBERNETES_SERVICES_LOADBALANCER_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.LoadBalancer.serviceTemplates || ['generic-service'], - hostTemplates: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_TEMPLATES || defaultConfig.kubernetes.services.LoadBalancer.hostTemplates || ['generic-host'], + discover: process.env.KUBERNETES_SERVICES_LOADBALANCER_DISCOVER || defaultConfig.kubernetes.services.LoadBalancer.discover, + hostName: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOSTNAME || defaultConfig.kubernetes.services.LoadBalancer.hostName, + applyServices: process.env.KUBERNETES_SERVICES_LOADBALANCER_APPLYSERVICES || defaultConfig.kubernetes.services.LoadBalancer.applyServices, + serviceDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_SERVICE_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.serviceDefinition, + hostDefinition: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_DEFINITION || defaultConfig.kubernetes.services.LoadBalancer.hostDefinition, + serviceTemplates: process.env.KUBERNETES_SERVICES_LOADBALANCER_SERVICE_TEMPLATES || defaultConfig.kubernetes.services.LoadBalancer.serviceTemplates, + hostTemplates: process.env.KUBERNETES_SERVICES_LOADBALANCER_HOST_TEMPLATES || defaultConfig.kubernetes.services.LoadBalancer.hostTemplates, }, }, }, diff --git a/src/icinga.ts b/src/icinga.ts index 96d03dc..bc83bc7 100644 --- a/src/icinga.ts +++ b/src/icinga.ts @@ -7,6 +7,7 @@ import IcingaClient from 'icinga2-api'; export default class Icinga { protected logger: Logger; protected icingaClient: IcingaClient; + protected triggerRestart: boolean=false; /** * icinga wrapper @@ -14,6 +15,28 @@ export default class Icinga { constructor(logger: Logger, icingaClient: IcingaClient) { this.logger = logger; this.icingaClient = icingaClient; + this.checkRestart(); + } + + /** + * Check if restarted needed + */ + protected checkRestart(): void { + setInterval(() => { + this.logger.debug(`check if icinga service restart is required (https://github.com/Icinga/icinga2/issues/6012)`); + if(this.triggerRestart === true) { + this.logger.debug(`icinga service restart required`); + this.triggerRestart = false; + + this.icingaClient.restartProcess((err, result) => { + if(err) { + this.logger.error(`trigger icinga service restart`, {error: err}); + } else { + this.logger.info(`icinga service restart triggered`); + } + }); + } + }, 30000); } /** @@ -53,6 +76,7 @@ export default class Icinga { resolve(false); } else { this.logger.info(`host group ${name} was created successfully`, {result: result}); + this.triggerRestart = true; resolve(true); } }); @@ -84,6 +108,7 @@ export default class Icinga { resolve(false); } else { this.logger.info(`service group ${name} was created successfully`, {result: result}); + this.triggerRestart = true; resolve(true); } }); @@ -120,6 +145,7 @@ export default class Icinga { resolve(false); } else { this.logger.info(`host ${name} was created successfully`, {result: result}); + this.triggerRestart = true; resolve(true); } }); @@ -156,6 +182,7 @@ export default class Icinga { resolve(false); } else { this.logger.info(`service ${name} on host ${host} was created successfully`, {result: result}); + this.triggerRestart = true; resolve(true); } }); diff --git a/src/kube/service.ts b/src/kube/service.ts index 3ebf5b3..f18c765 100644 --- a/src/kube/service.ts +++ b/src/kube/service.ts @@ -141,6 +141,11 @@ export default class Service extends Resource { let hasCommand = await this.icinga.hasCheckCommand(port.check_command); if (hasCommand) { this.logger.debug('service can be checked via check command '+port.check_command); + + if (serviceType !== Service.TYPE_NODEPORT) { + port['vars.'+port.check_command+'_address'] = definition.spec.clusterIP; + } + port['vars.'+port.check_command+'_port'] = servicePort.nodePort || servicePort.port; } else { delete port.check_command; @@ -153,6 +158,11 @@ export default class Service extends Resource { if (!port.check_command) { port.check_command = protocol; + + if (serviceType !== Service.TYPE_NODEPORT) { + port['vars.'+protocol+'_address'] = definition.spec.clusterIP; + } + port['vars.'+protocol+'_port'] = servicePort.nodePort || servicePort.port; } diff --git a/src/logger.ts b/src/logger.ts index 637c0e4..2e054b7 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -2,7 +2,7 @@ import {createLogger, format, transports} from 'winston'; import config from './config'; const logger = createLogger({ - level: config.log.level, + level: config.log.level || 'info', format: format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss', diff --git a/src/main.ts b/src/main.ts index 4241c0d..bfcb3fb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,7 +28,7 @@ async function main() { logger.error('failed to cleanup icinga hosts', {error: err}); }); } - + if (config.kubernetes.nodes.discover) { kubeNode.kubeListener(function() { let json = new JSONStream(); @@ -48,7 +48,7 @@ async function main() { } if (config.kubernetes.volumes.discover) { - kubeVolume.kubeListener(function() { + kubeVolume.kubeListener(() => { let json = new JSONStream(); let stream = kubeClient.apis.v1.watch.persistentvolumes.getStream(); stream.pipe(json); @@ -59,7 +59,7 @@ async function main() { if (config.kubernetes.services.ClusterIP.discover || config.kubernetes.services.NodePort.discover || config.kubernetes.services.LoadBalancer.discover) { - kubeService.kubeListener(function() { + kubeService.kubeListener(() => { let json = new JSONStream(); let stream = kubeClient.apis.v1.watch.services.getStream(); stream.pipe(json); diff --git a/tests/icinga.test.ts b/tests/icinga.test.ts index 08a7601..4d2511c 100644 --- a/tests/icinga.test.ts +++ b/tests/icinga.test.ts @@ -6,7 +6,7 @@ jest.mock('../src/logger'); var icinga; beforeEach(() => { - icinga = new IcingaClient(Logger, IcingaApi); + icinga = new IcingaClient(Logger, IcingaApi); }); describe('icinga', () => { @@ -238,8 +238,41 @@ describe('icinga', () => { expect(calls.length).toBe(1); expect(calls[0][1]).toBe('foobar'); }); - }); + it('add new host restarts icinga service', async () => { + IcingaApi.restartProcess = jest.fn() + .mockImplementation((cb) => cb(null, {foo: "bar"})); + + jest.useFakeTimers(); + icinga = new IcingaClient(Logger, IcingaApi); + + IcingaApi.createHostCustom = jest.fn() + .mockImplementation((data, name, cb) => cb(null, {foo: "bar"})); + + IcingaApi.getHostState = jest.fn() + .mockImplementation((Host, cb) => cb({Statuscode: 404}, null)); + + await expect(icinga.applyHost('foobar', {foo: "bar"}, ["foobar"])).resolves.toEqual(true); + + var calls = IcingaApi.getHostState.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe('foobar'); + + calls = IcingaApi.createHostCustom.mock.calls; + expect(calls.length).toBe(1); + expect(calls[0][0]).toBe(JSON.stringify({ + attrs: { + foo: "bar" + }, + templates: ["foobar"] + })); + + expect(calls[0][1]).toBe('foobar'); + jest.runOnlyPendingTimers(); + expect(IcingaApi.restartProcess.mock.calls.length).toBe(1); + }); + }); + describe('apply service', () => { it('icinga service does exists', async () => { IcingaApi.createServiceCustom = jest.fn(); diff --git a/tests/kube/service.test.ts b/tests/kube/service.test.ts index 7513bbd..5e26641 100644 --- a/tests/kube/service.test.ts +++ b/tests/kube/service.test.ts @@ -305,8 +305,10 @@ describe('kubernetes services', () => { expect(calls[1][1]).toBe('foobar-foo-bar'); expect(calls[0][2]['check_command']).toBe('tcp'); + expect(calls[0][2]['vars.tcp_address']).toBe('10.99.24.32'); expect(calls[0][2]['vars.tcp_port']).toBe(80); expect(calls[1][2]['check_command']).toBe('tcp'); + expect(calls[1][2]['vars.tcp_address']).toBe('10.99.24.32'); expect(calls[1][2]['vars.tcp_port']).toBe(10000); }); @@ -347,8 +349,12 @@ describe('kubernetes services', () => { const calls = Icinga.applyService.mock.calls; expect(Icinga.applyService.mock.instances.length).toBe(2); expect(calls[0][2].check_command).toBe('http'); + expect(calls[0][2]['vars.http_address']).toBe('10.99.24.32'); + expect(calls[0][2]['vars.http_port']).toBe(80); expect(calls[0][2]['vars.foo']).toBe('bar'); expect(calls[1][2].check_command).toBe('http'); + expect(calls[1][2]['vars.http_address']).toBe('10.99.24.32'); + expect(calls[1][2]['vars.http_port']).toBe(10000); expect(calls[1][2]['vars.foo']).toBe('bar'); }); @@ -394,6 +400,46 @@ describe('kubernetes services', () => { expect(calls[0][3]).toEqual(['foo', 'bar']); expect(calls[1][3]).toEqual(['foo', 'bar']); }); + + it('create NodePort service objects', async () => { + let instance = new Service(Logger, Node, Icinga); + + Node.getWorkerNodes = function() { + return ['foo', 'bar']; + }; + + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + fixture.spec.type = 'NodePort' + Icinga.applyHost = jest.fn(); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyHost.mock.instances.length).toBe(0); + expect(Icinga.applyService.mock.instances.length).toBe(4); + + expect(calls[0][0]).toBe('foo'); + expect(calls[1][0]).toBe('bar'); + expect(calls[0][1]).toBe('foobar-foo-http'); + expect(calls[1][1]).toBe('foobar-foo-http'); + expect(calls[0][2]['check_command']).toBe('tcp'); + expect(calls[0][2]['vars.tcp_address']).toBe(undefined); + expect(calls[0][2]['vars.tcp_port']).toBe(80); + expect(calls[1][2]['check_command']).toBe('tcp'); + expect(calls[1][2]['vars.tcp_address']).toBe(undefined); + expect(calls[1][2]['vars.tcp_port']).toBe(80); + + expect(calls[2][0]).toBe('foo'); + expect(calls[3][0]).toBe('bar'); + expect(calls[2][1]).toBe('foobar-foo-bar'); + expect(calls[3][1]).toBe('foobar-foo-bar'); + expect(calls[2][2]['check_command']).toBe('tcp'); + expect(calls[2][2]['vars.tcp_address']).toBe(undefined); + expect(calls[2][2]['vars.tcp_port']).toBe(10000); + expect(calls[3][2]['check_command']).toBe('tcp'); + expect(calls[3][2]['vars.tcp_address']).toBe(undefined); + expect(calls[3][2]['vars.tcp_port']).toBe(10000); + }); }); describe('kubernetes annotations', () => { @@ -417,6 +463,10 @@ describe('kubernetes services', () => { expect(Icinga.applyService.mock.instances.length).toBe(2); expect(calls[0][2].check_command).toBe('bar'); expect(calls[1][2].check_command).toBe('bar'); + expect(calls[0][2]['vars.bar_address']).toBe('10.99.24.32'); + expect(calls[0][2]['vars.bar_port']).toBe(80); + expect(calls[1][2]['vars.bar_address']).toBe('10.99.24.32'); + expect(calls[1][2]['vars.bar_port']).toBe(10000); expect(calls[0][3]).toEqual(['foobar', 'barfoo']); expect(calls[1][3]).toEqual(['foobar', 'barfoo']); }); From e5dcc57f0c279c7de224a314c469fe338654bcd7 Mon Sep 17 00:00:00 2001 From: raffis Date: Thu, 21 Feb 2019 11:44:34 +0100 Subject: [PATCH 15/15] added new tests --- tests/icinga.test.ts | 47 +++++++++++++++++++++++++++++ tests/kube/ingress.test.ts | 62 ++++++++++++++++++++++++++++++++++++++ tests/kube/volume.test.ts | 61 ++++++++++++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 1 deletion(-) diff --git a/tests/icinga.test.ts b/tests/icinga.test.ts index 4d2511c..35daf47 100644 --- a/tests/icinga.test.ts +++ b/tests/icinga.test.ts @@ -391,4 +391,51 @@ describe('icinga', () => { expect(calls[0][0]).toBe('bar'); }); }); + + + describe('delete services by filter', () => { + it('delete service filtered', async () => { + icinga.deleteService = jest.fn(); + IcingaApi.getServiceFiltered = jest.fn() + .mockImplementation((host, cb) => cb(null, [ + {attrs: {name: "foo", host_name: "foo"}}, + {attrs: {name: "bar", host_name: "bar"}}, + ])); + + await expect(icinga.deleteServicesByFilter('vars.bar==true')).resolves.toEqual(true); + expect(icinga.deleteService.mock.calls.length).toBe(2); + }); + + it('get services filtered failed', async () => { + IcingaApi.deleteService = jest.fn(); + IcingaApi.getServiceFiltered = jest.fn() + .mockImplementation((host, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.deleteServicesByFilter('vars.bar==true')).rejects.toEqual({Statuscode: 500}); + expect(IcingaApi.deleteService.mock.calls.length).toBe(0); + }); + }); + + describe('delete hosts by filter', () => { + it('delete host filtered', async () => { + icinga.deleteHost = jest.fn(); + IcingaApi.getHostFiltered = jest.fn() + .mockImplementation((host, cb) => cb(null, [ + {attrs: {name: "foo"}}, + {attrs: {name: "bar"}}, + ])); + + await expect(icinga.deleteHostsByFilter('vars.bar==true')).resolves.toEqual(true); + expect(icinga.deleteHost.mock.calls.length).toBe(2); + }); + + it('get hosts filtered failed', async () => { + IcingaApi.deleteHost = jest.fn(); + IcingaApi.getHostFiltered = jest.fn() + .mockImplementation((host, cb) => cb({Statuscode: 500}, null)); + + await expect(icinga.deleteHostsByFilter('vars.bar==true')).rejects.toEqual({Statuscode: 500}); + expect(IcingaApi.deleteHost.mock.calls.length).toBe(0); + }); + }); }); diff --git a/tests/kube/ingress.test.ts b/tests/kube/ingress.test.ts index c9c04ad..0f71e42 100644 --- a/tests/kube/ingress.test.ts +++ b/tests/kube/ingress.test.ts @@ -114,6 +114,41 @@ describe('kubernetes ingresses', () => { await bindings.data(resource); expect(Icinga.applyHost.mock.calls.length).toBe(1); }); + + it('modify ingress object delete and create host', async () => { + let instance = new Ingress(Logger, Node, Icinga, { + hostName: null + }); + + var resource = { + type: 'MODIFIED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = jest.fn(); + Icinga.deleteHost = function(name) { + expect(name).toEqual('ingress-foobar-foo'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + var bindings = {}; + var json = { + on: async function(name, callback) { + bindings[name] = callback; + } + }; + + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + expect(Icinga.deleteServicesByFilter.mock.calls.length).toBe(0); + }); it('delete ingress object delete', async () => { let instance = new Ingress(Logger, Node, Icinga); @@ -321,6 +356,33 @@ describe('kubernetes ingresses', () => { expect(calls[1][2]['vars.http_ssl']).toBe(true); }); + it('attach services to kube workers if attachToNodes is enabled', async () => { + let instance = new Ingress(Logger, Node, Icinga, { + attachToNodes: true + }); + + Node.getWorkerNodes = function() { + return ['foo', 'bar']; + }; + + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyHost = jest.fn(); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyHost.mock.instances.length).toBe(0); + expect(Icinga.applyService.mock.instances.length).toBe(4); + + expect(calls[0][0]).toBe('foo'); + expect(calls[1][0]).toBe('bar'); + expect(calls[2][0]).toBe('foo'); + expect(calls[3][0]).toBe('bar'); + expect(calls[0][1]).toBe('foobar.example.org-http--'); + expect(calls[1][1]).toBe('foobar.example.org-http--'); + expect(calls[2][1]).toBe('barfoo.example.org-http--foo'); + expect(calls[3][1]).toBe('barfoo.example.org-http--foo'); + }); }); describe('kubernetes annotations', () => { diff --git a/tests/kube/volume.test.ts b/tests/kube/volume.test.ts index d28cd26..857dc38 100644 --- a/tests/kube/volume.test.ts +++ b/tests/kube/volume.test.ts @@ -109,7 +109,42 @@ describe('kubernetes volumes', () => { await bindings.data(resource); expect(Icinga.applyHost.mock.calls.length).toBe(1); }); + + it('modify ingress object delete and create host', async () => { + let instance = new Volume(Logger, Node, Icinga, { + hostName: null + }); + + var resource = { + type: 'MODIFIED', + object: fixture + }; + + Icinga.applyHost = jest.fn(); + Icinga.deleteServicesByFilter = jest.fn(); + Icinga.deleteHost = function(name) { + expect(name).toEqual('volume-generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + return new Promise((resolve,reject) => { + resolve(true); + }); + }; + + var bindings = {}; + var json = { + on: async function(name, callback) { + bindings[name] = callback; + } + }; + await instance.kubeListener(() => { + return json; + }); + + await bindings.data(resource); + expect(Icinga.applyHost.mock.calls.length).toBe(1); + expect(Icinga.deleteServicesByFilter.mock.calls.length).toBe(0); + }); + it('delete volume object delete', async () => { let instance = new Volume(Logger, Node, Icinga); @@ -233,7 +268,7 @@ describe('kubernetes volumes', () => { }); }); - describe('add all volume object http path rules as service objects', () => { + describe('add all volume objects as service objects', () => { it('create service object', async () => { let instance = new Volume(Logger, Node, Icinga); @@ -288,6 +323,30 @@ describe('kubernetes volumes', () => { expect(Icinga.applyService.mock.instances.length).toBe(1); expect(calls[0][3]).toEqual(['foo', 'bar']); }); + + it('attach services to kube workers if attachToNodes is enabled', async () => { + let instance = new Volume(Logger, Node, Icinga, { + attachToNodes: true + }); + + Node.getWorkerNodes = function() { + return ['foo', 'bar']; + }; + + Icinga.applyService = jest.fn(); + Icinga.applyServiceGroup = jest.fn(); + Icinga.applyHost = jest.fn(); + + await instance.prepareObject(fixture); + const calls = Icinga.applyService.mock.calls; + expect(Icinga.applyHost.mock.instances.length).toBe(0); + expect(Icinga.applyService.mock.instances.length).toBe(2); + + expect(calls[0][0]).toBe('foo'); + expect(calls[1][0]).toBe('bar'); + expect(calls[0][1]).toBe('generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + expect(calls[1][1]).toBe('generic-nimble-fad5684e-22fb-11e9-94e3-0050568fe3c2'); + }); }); describe('kubernetes annotations', () => {