From 4d247356a7c50187a4b6558ce79742c5499cb2dc Mon Sep 17 00:00:00 2001 From: Zhiyuan He <362583303@qq.com> Date: Wed, 22 Jul 2020 22:20:20 +0800 Subject: [PATCH 01/32] Compact PR (#4758) * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * save * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * save * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix --- .travis.yml | 4 + .../services-configuration.yaml.template | 6 - src/database-controller/.editorconfig | 9 + src/database-controller/.gitignore | 106 + src/database-controller/README.md | 8 + src/database-controller/build/build-pre.sh | 26 + .../database-controller.common.dockerfile | 35 + .../config/database-controller.yaml | 32 + .../config/database_controller.py | 53 + .../deploy/database-controller.yaml.template | 116 + .../deploy/database-initializer.yaml.template | 61 + src/database-controller/deploy/delete.sh | 26 + src/database-controller/deploy/rbac.yaml | 36 + src/database-controller/deploy/refresh.sh | 26 + src/database-controller/deploy/service.yaml | 35 + .../deploy/start.sh.template | 35 + src/database-controller/deploy/stop.sh | 36 + src/database-controller/sdk/.dockerignore | 63 + src/database-controller/sdk/index.js | 266 +++ src/database-controller/sdk/package.json | 14 + src/database-controller/sdk/yarn.lock | 241 +++ src/database-controller/src/.dockerignore | 63 + src/database-controller/src/core/config.js | 56 + src/database-controller/src/core/framework.js | 372 ++++ src/database-controller/src/core/k8s.js | 207 ++ src/database-controller/src/core/logger.js | 57 + src/database-controller/src/core/util.js | 82 + src/database-controller/src/index.js | 23 + .../src/initializer/index.js | 71 + src/database-controller/src/package.json | 35 + src/database-controller/src/poller/config.js | 48 + src/database-controller/src/poller/index.js | 129 ++ .../src/watcher/framework/config.js | 37 + .../src/watcher/framework/index.js | 69 + .../src/write-merger/app.js | 55 + .../src/write-merger/config.js | 46 + .../src/write-merger/handler.js | 168 ++ .../src/write-merger/index.js | 23 + src/database-controller/src/yarn.lock | 1862 +++++++++++++++++ src/database-controller/test-case.md | 146 ++ src/fluentd/config/fluentd.py | 3 - .../lib/fluent/plugin/out_pgjson.rb | 9 +- src/internal-storage/README.md | 1 - .../config/internal-storage.yaml | 1 - .../config/internal_storage.py | 30 +- .../deploy/delete.sh.template | 4 - src/internal-storage/deploy/start.sh.template | 4 - src/postgresql/README.md | 2 +- src/postgresql/config/postgresql.py | 20 +- src/postgresql/config/postgresql.yaml | 4 +- .../deploy/postgresql.yaml.template | 2 +- src/postgresql/deploy/start.sh.template | 4 - src/postgresql/src/init_table.sql | 22 +- src/rest-server/.gitignore | 2 + src/rest-server/build/build-pre.sh | 1 + src/rest-server/config/rest-server.yaml | 1 + src/rest-server/config/rest_server.py | 2 +- .../deploy/rest-server.yaml.template | 6 +- src/rest-server/deploy/service.yaml | 1 + src/rest-server/docs/swagger.yaml | 62 +- src/rest-server/package.json | 2 + src/rest-server/src/config/launcher.js | 7 + src/rest-server/src/controllers/v2/job.js | 53 +- src/rest-server/src/models/v2/job-attempt.js | 106 +- src/rest-server/src/models/v2/job/k8s.js | 367 ++-- .../src/utils/{postgresUtil.js => dbUtils.js} | 26 +- src/rest-server/test/setup.js | 3 + src/rest-server/text | 17 + src/rest-server/yarn.lock | 37 +- src/webportal/src/app/components/util/job.js | 3 +- .../job/job-view/fabric/JobList/Ordering.js | 8 +- .../app/job/job-view/fabric/JobList/Table.jsx | 10 +- .../app/job/job-view/fabric/JobList/utils.js | 10 +- .../fabric/job-detail/components/summary.jsx | 6 +- 74 files changed, 5190 insertions(+), 429 deletions(-) create mode 100644 src/database-controller/.editorconfig create mode 100644 src/database-controller/.gitignore create mode 100644 src/database-controller/README.md create mode 100644 src/database-controller/build/build-pre.sh create mode 100644 src/database-controller/build/database-controller.common.dockerfile create mode 100644 src/database-controller/config/database-controller.yaml create mode 100644 src/database-controller/config/database_controller.py create mode 100644 src/database-controller/deploy/database-controller.yaml.template create mode 100644 src/database-controller/deploy/database-initializer.yaml.template create mode 100644 src/database-controller/deploy/delete.sh create mode 100644 src/database-controller/deploy/rbac.yaml create mode 100644 src/database-controller/deploy/refresh.sh create mode 100644 src/database-controller/deploy/service.yaml create mode 100644 src/database-controller/deploy/start.sh.template create mode 100644 src/database-controller/deploy/stop.sh create mode 100644 src/database-controller/sdk/.dockerignore create mode 100644 src/database-controller/sdk/index.js create mode 100644 src/database-controller/sdk/package.json create mode 100644 src/database-controller/sdk/yarn.lock create mode 100644 src/database-controller/src/.dockerignore create mode 100644 src/database-controller/src/core/config.js create mode 100644 src/database-controller/src/core/framework.js create mode 100644 src/database-controller/src/core/k8s.js create mode 100644 src/database-controller/src/core/logger.js create mode 100644 src/database-controller/src/core/util.js create mode 100644 src/database-controller/src/index.js create mode 100644 src/database-controller/src/initializer/index.js create mode 100644 src/database-controller/src/package.json create mode 100644 src/database-controller/src/poller/config.js create mode 100644 src/database-controller/src/poller/index.js create mode 100644 src/database-controller/src/watcher/framework/config.js create mode 100644 src/database-controller/src/watcher/framework/index.js create mode 100644 src/database-controller/src/write-merger/app.js create mode 100644 src/database-controller/src/write-merger/config.js create mode 100644 src/database-controller/src/write-merger/handler.js create mode 100644 src/database-controller/src/write-merger/index.js create mode 100644 src/database-controller/src/yarn.lock create mode 100644 src/database-controller/test-case.md rename src/rest-server/src/utils/{postgresUtil.js => dbUtils.js} (77%) create mode 100644 src/rest-server/text diff --git a/.travis.yml b/.travis.yml index 44b8d844cd..3c895e7af1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -91,8 +91,10 @@ matrix: env: NODE_ENV=test before_install: - cd src/rest-server + - cp -rf ../database-controller/sdk openpaidbsdk install: - yarn install + - rm -rf openpaidbsdk script: - npm test - npm run coveralls @@ -102,8 +104,10 @@ matrix: env: NODE_ENV=test before_install: - cd src/rest-server + - cp -rf ../database-controller/sdk openpaidbsdk install: - yarn install --ignore-engines + - rm -rf openpaidbsdk script: - npm test diff --git a/contrib/kubespray/quick-start/services-configuration.yaml.template b/contrib/kubespray/quick-start/services-configuration.yaml.template index 593cec6eab..f665c2eb6e 100644 --- a/contrib/kubespray/quick-start/services-configuration.yaml.template +++ b/contrib/kubespray/quick-start/services-configuration.yaml.template @@ -48,12 +48,6 @@ rest-server: webportal: server-port: 9286 -internal-storage: - enable: true - -postgresql: - enable: true - #If you want to customize the scheduling config, such add more virtual clusters or more gpu types, check: #https://github.com/microsoft/pai/blob/master/docs/hivedscheduler/devops.md hivedscheduler: diff --git a/src/database-controller/.editorconfig b/src/database-controller/.editorconfig new file mode 100644 index 0000000000..0f1786729b --- /dev/null +++ b/src/database-controller/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/src/database-controller/.gitignore b/src/database-controller/.gitignore new file mode 100644 index 0000000000..06c379450a --- /dev/null +++ b/src/database-controller/.gitignore @@ -0,0 +1,106 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +version/ diff --git a/src/database-controller/README.md b/src/database-controller/README.md new file mode 100644 index 0000000000..e96c522c94 --- /dev/null +++ b/src/database-controller/README.md @@ -0,0 +1,8 @@ +## Database Controller + +### Development + +**Environment:** Node.js 8.17.0, use `yarn install` to install all dependencies under `src/` or `sdk/`. To set environmental variables, create a `.env` file under `src`. + + +**Lint:** Use `npm install standard --global` to isntall Standard.js globally. Then run `npm run lint` under `src/` or `sdk/`. diff --git a/src/database-controller/build/build-pre.sh b/src/database-controller/build/build-pre.sh new file mode 100644 index 0000000000..e1e0054cf6 --- /dev/null +++ b/src/database-controller/build/build-pre.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pushd $(dirname "$0") > /dev/null + +mkdir -p ../version +cp ../../../version/PAI.VERSION ../version/ +echo `git rev-parse HEAD` > ../version/COMMIT.VERSION + +popd > /dev/null diff --git a/src/database-controller/build/database-controller.common.dockerfile b/src/database-controller/build/database-controller.common.dockerfile new file mode 100644 index 0000000000..46c72bbc0d --- /dev/null +++ b/src/database-controller/build/database-controller.common.dockerfile @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +FROM node:carbon + +WORKDIR /database-controller + +COPY ./src ./src +COPY ./sdk ./sdk +COPY ./version ./version + +WORKDIR src + +RUN yarn install + +RUN npm install json -g +RUN json -I -f package.json -e "this.paiVersion=\"`cat ../version/PAI.VERSION`\"" +RUN json -I -f package.json -e "this.paiCommitVersion=\"`cat ../version/COMMIT.VERSION`\"" + + +CMD ["sleep", "infinity"] diff --git a/src/database-controller/config/database-controller.yaml b/src/database-controller/config/database-controller.yaml new file mode 100644 index 0000000000..0445c21113 --- /dev/null +++ b/src/database-controller/config/database-controller.yaml @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +service_type: "common" + +# general settings +log-level: info +recovery-mode: false +k8s-connection-timeout-second: 120 +write-merger-connection-timeout-second: 120 + +# write merger +write-merger-port: 9748 +write-merger-max-db-connection: 50 + +# db poller +db-poller-interval-second: 120 +db-poller-max-db-connection: 10 diff --git a/src/database-controller/config/database_controller.py b/src/database-controller/config/database_controller.py new file mode 100644 index 0000000000..5bbb96d866 --- /dev/null +++ b/src/database-controller/config/database_controller.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import copy +import logging + + +class DatabaseController(object): + def __init__(self, cluster_conf, service_conf, default_service_conf): + self.cluster_conf = cluster_conf + self.service_conf = self.merge_service_configuration(service_conf, default_service_conf) + self.logger = logging.getLogger(__name__) + + @staticmethod + def merge_service_configuration(overwrite_srv_cfg, default_srv_cfg): + if overwrite_srv_cfg is None: + return default_srv_cfg + srv_cfg = default_srv_cfg.copy() + for k in overwrite_srv_cfg: + srv_cfg[k] = overwrite_srv_cfg[k] + return srv_cfg + + def get_master_ip(self): + for host_conf in self.cluster_conf["machine-list"]: + if "pai-master" in host_conf and host_conf["pai-master"] == "true": + return host_conf["hostip"] + + def validation_pre(self): + return True, None + + def run(self): + result = copy.deepcopy(self.service_conf) + result['write-merger-url'] = 'http://{}:{}'.format(self.get_master_ip(), result['write-merger-port']) + return result + + def validation_post(self, conf): + return True, None diff --git a/src/database-controller/deploy/database-controller.yaml.template b/src/database-controller/deploy/database-controller.yaml.template new file mode 100644 index 0000000000..ae4558bbd9 --- /dev/null +++ b/src/database-controller/deploy/database-controller.yaml.template @@ -0,0 +1,116 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: database-controller-sts +spec: + selector: + matchLabels: + app: database-controller + serviceName: database-controller + replicas: 1 + template: + metadata: + labels: + app: database-controller + spec: + serviceAccountName: database-controller-account + hostNetwork: true + containers: + - name: write-merger + image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} + imagePullPolicy: Always + env: + - name: LOG_LEVEL + value: "{{ cluster_cfg['database-controller']['log-level'] }}" + - name: K8S_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + - name: RECOVERY_MODE_ENABLED +{% if cluster_cfg['database-controller']['recovery-mode'] %} + value: "true" +{% else %} + value: "false" +{% endif %} + - name: DB_CONNECTION_STR + value: "{{ cluster_cfg['postgresql']['connection-str'] }}" + - name: MAX_DB_CONNECTION + value: "{{ cluster_cfg['database-controller']['write-merger-max-db-connection'] }}" + - name: PORT + value: "{{ cluster_cfg['database-controller']['write-merger-port'] }}" + readinessProbe: + httpGet: + path: /api/v1/ping + port: {{ cluster_cfg['database-controller']['write-merger-port'] }} + livenessProbe: + httpGet: + path: /api/v1/ping + port: {{ cluster_cfg['database-controller']['write-merger-port'] }} + initialDelaySeconds: 60 + periodSeconds: 60 + command: ["node", "write-merger/index.js"] + - name: framework-watcher + image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} + imagePullPolicy: Always + env: + - name: LOG_LEVEL + value: "{{ cluster_cfg['database-controller']['log-level'] }}" + - name: K8S_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + - name: RECOVERY_MODE_ENABLED +{% if cluster_cfg['database-controller']['recovery-mode'] %} + value: "true" +{% else %} + value: "false" +{% endif %} + - name: WRITE_MERGER_URL + value: "{{ cluster_cfg['database-controller']['write-merger-url'] }}" + command: ["node", "watcher/framework/index.js"] + - name: poller + image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} + imagePullPolicy: Always + env: + - name: LOG_LEVEL + value: "{{ cluster_cfg['database-controller']['log-level'] }}" + - name: K8S_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + - name: RECOVERY_MODE_ENABLED +{% if cluster_cfg['database-controller']['recovery-mode'] %} + value: "true" +{% else %} + value: "false" +{% endif %} + - name: DB_CONNECTION_STR + value: "{{ cluster_cfg['postgresql']['connection-str'] }}" + - name: MAX_DB_CONNECTION + value: "{{ cluster_cfg['database-controller']['db-poller-max-db-connection'] }}" + - name: INTERVAL_SECOND + value: "{{ cluster_cfg['database-controller']['db-poller-interval-second'] }}" + - name: WRITE_MERGER_URL + value: "{{ cluster_cfg['database-controller']['write-merger-url'] }}" + command: ["node", "poller/index.js"] + imagePullSecrets: + - name: {{ cluster_cfg["cluster"]["docker-registry"]["secret-name"] }} diff --git a/src/database-controller/deploy/database-initializer.yaml.template b/src/database-controller/deploy/database-initializer.yaml.template new file mode 100644 index 0000000000..3186db0ab6 --- /dev/null +++ b/src/database-controller/deploy/database-initializer.yaml.template @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: database-initializer-sts +spec: + selector: + matchLabels: + app: database-initializer + serviceName: database-initializer + replicas: 1 + template: + metadata: + labels: + app: database-initializer + spec: + serviceAccountName: database-controller-account + hostNetwork: true + containers: + - name: database-initializer + image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} + imagePullPolicy: Always + env: + - name: LOG_LEVEL + value: "{{ cluster_cfg['database-controller']['log-level'] }}" + - name: K8S_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + - name: RECOVERY_MODE_ENABLED +{% if cluster_cfg['database-controller']['recovery-mode'] %} + value: "true" +{% else %} + value: "false" +{% endif %} + - name: DB_CONNECTION_STR + value: "{{ cluster_cfg['postgresql']['connection-str'] }}" + command: ["node", "initializer/index.js"] + readinessProbe: + exec: + command: + - ls + - /READY + imagePullSecrets: + - name: {{ cluster_cfg["cluster"]["docker-registry"]["secret-name"] }} diff --git a/src/database-controller/deploy/delete.sh b/src/database-controller/deploy/delete.sh new file mode 100644 index 0000000000..1b952d11cc --- /dev/null +++ b/src/database-controller/deploy/delete.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pushd $(dirname "$0") > /dev/null + +echo "Call stop script to stop all service first" +/bin/bash stop.sh || exit $? + + +popd > /dev/null \ No newline at end of file diff --git a/src/database-controller/deploy/rbac.yaml b/src/database-controller/deploy/rbac.yaml new file mode 100644 index 0000000000..52cd13471f --- /dev/null +++ b/src/database-controller/deploy/rbac.yaml @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: database-controller-account + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: database-controller-role-binding + namespace: default +subjects: + - kind: ServiceAccount + name: database-controller-account + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin diff --git a/src/database-controller/deploy/refresh.sh b/src/database-controller/deploy/refresh.sh new file mode 100644 index 0000000000..3b6d2ca17d --- /dev/null +++ b/src/database-controller/deploy/refresh.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +pushd $(dirname "$0") > /dev/null + +bash stop.sh +bash start.sh + +popd > /dev/null diff --git a/src/database-controller/deploy/service.yaml b/src/database-controller/deploy/service.yaml new file mode 100644 index 0000000000..b3bc1d10ea --- /dev/null +++ b/src/database-controller/deploy/service.yaml @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +cluster-type: + - k8s + +prerequisite: + - postgresql + +template-list: + - database-initializer.yaml + - database-controller.yaml + - start.sh + +start-script: start.sh +stop-script: stop.sh +delete-script: delete.sh +refresh-script: refresh.sh + +deploy-rules: + - in: pai-master diff --git a/src/database-controller/deploy/start.sh.template b/src/database-controller/deploy/start.sh.template new file mode 100644 index 0000000000..4be1562eda --- /dev/null +++ b/src/database-controller/deploy/start.sh.template @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pushd $(dirname "$0") > /dev/null + +kubectl apply --overwrite=true -f rbac.yaml || exit $? + +kubectl apply --overwrite=true -f database-initializer.yaml || exit $? +echo 'Database is being initialized, please wait...' +PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ready_status -w -k app -v database-initializer || exit $? +PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.maintaintool.update_resource \ + --operation delete --resource statefulset --name database-initializer-sts + + +kubectl apply --overwrite=true -f database-controller.yaml || exit $? +# Wait until the service is ready. +PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ready_status -w -k app -v database-controller || exit $? + +popd > /dev/null diff --git a/src/database-controller/deploy/stop.sh b/src/database-controller/deploy/stop.sh new file mode 100644 index 0000000000..881287903d --- /dev/null +++ b/src/database-controller/deploy/stop.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation +# All rights reserved. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +# to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pushd $(dirname "$0") > /dev/null + +PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.maintaintool.update_resource \ + --operation delete --resource statefulset --name database-initializer-sts + +PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.maintaintool.update_resource \ + --operation delete --resource statefulset --name database-controller-sts + +if kubectl get clusterrolebinding | grep -q "database-controller-role-binding"; then + kubectl delete clusterrolebinding database-controller-role-binding || exit $? +fi + +if kubectl get serviceaccount | grep -q "database-controller-account"; then + kubectl delete serviceaccount database-controller-account || exit $? +fi + +popd > /dev/null diff --git a/src/database-controller/sdk/.dockerignore b/src/database-controller/sdk/.dockerignore new file mode 100644 index 0000000000..210d8fd1c6 --- /dev/null +++ b/src/database-controller/sdk/.dockerignore @@ -0,0 +1,63 @@ +.git + +# Directory for submitted jobs' json file and scripts +frameworklauncher/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env diff --git a/src/database-controller/sdk/index.js b/src/database-controller/sdk/index.js new file mode 100644 index 0000000000..a989bd587a --- /dev/null +++ b/src/database-controller/sdk/index.js @@ -0,0 +1,266 @@ +const { Sequelize, Model } = require('sequelize') + +class DatabaseModel { + constructor (connectionStr, maxConnection = 10) { + const sequelize = new Sequelize( + connectionStr, + { + pool: { + max: maxConnection, + min: 1 + } + } + ) + + class Framework extends Model {} + Framework.init({ + insertedAt: Sequelize.DATE, + name: { + type: Sequelize.STRING(64), + primaryKey: true + }, + namespace: Sequelize.STRING(64), + jobName: Sequelize.STRING(256), + userName: Sequelize.STRING(256), + jobConfig: Sequelize.TEXT, + executionType: Sequelize.STRING(32), + creationTime: Sequelize.DATE, + virtualCluster: Sequelize.STRING(256), + jobPriority: Sequelize.STRING(256), + totalGpuNumber: Sequelize.INTEGER, + totalTaskNumber: Sequelize.INTEGER, + totalTaskRoleNumber: Sequelize.INTEGER, + logPathInfix: Sequelize.STRING(256), + submissionTime: { + type: Sequelize.DATE, + allowNull: false + }, + dockerSecretDef: Sequelize.TEXT, + configSecretDef: Sequelize.TEXT, + priorityClassDef: Sequelize.TEXT, + retries: Sequelize.INTEGER, + retryDelayTime: Sequelize.INTEGER, + platformRetries: Sequelize.INTEGER, + resourceRetries: Sequelize.INTEGER, + userRetries: Sequelize.INTEGER, + completionTime: Sequelize.DATE, + appExitCode: Sequelize.INTEGER, + subState: Sequelize.STRING(32), + state: Sequelize.STRING(32), + snapshot: Sequelize.TEXT, + requestSynced: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false + }, + apiServerDeleted: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false + }, + archived: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false + } + }, { + sequelize, + indexes: [{ + unique: false, + fields: ['submissionTime'] + }], + modelName: 'framework', + createdAt: 'insertedAt' + }) + + class FrameworkHistory extends Model {} + FrameworkHistory.init({ + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false + }, + attemptIndex: Sequelize.INTEGER, + historyType: { + type: Sequelize.STRING(16), + allowNull: false, + defaultValue: 'retry' + }, + snapshot: Sequelize.TEXT + }, { + sequelize, + modelName: 'framework_history', + createdAt: 'insertedAt', + indexes: [{ + unique: false, + fields: ['frameworkName'] + }], + freezeTableName: true + }) + + class Pod extends Model {} + Pod.init({ + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false + }, + attemptIndex: Sequelize.INTEGER, + taskroleName: Sequelize.STRING(256), + taskroleIndex: Sequelize.INTEGER, + taskAttemptIndex: Sequelize.INTEGER, + snapshot: Sequelize.TEXT + }, { + sequelize, + modelName: 'pod', + createdAt: 'insertedAt', + indexes: [{ + unique: false, + fields: ['frameworkName'] + }] + }) + + class FrameworkEvent extends Model {} + FrameworkEvent.init({ + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false + }, + type: { + type: Sequelize.STRING(32), + allowNull: false + }, + message: Sequelize.TEXT, + event: Sequelize.TEXT + }, { + sequelize, + modelName: 'framework_event', + createdAt: 'insertedAt', + indexes: [{ + unique: false, + fields: ['frameworkName'] + }] + }) + + class PodEvent extends Model {} + PodEvent.init({ + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false + }, + podUid: { + type: Sequelize.STRING(36), + allowNull: false + }, + type: { + type: Sequelize.STRING(32), + allowNull: false + }, + message: Sequelize.TEXT, + event: Sequelize.TEXT + }, { + sequelize, + modelName: 'pod_event', + createdAt: 'insertedAt', + indexes: [{ + unique: false, + fields: ['frameworkName'] + }] + }) + + Framework.hasMany(FrameworkHistory) + Framework.hasMany(Pod) + Framework.hasMany(FrameworkEvent) + Framework.hasMany(PodEvent) + + class Version extends Model {} + Version.init({ + version: { + type: Sequelize.STRING(36) + }, + commitVersion: { + type: Sequelize.STRING(64) + } + }, { + sequelize, + modelName: 'version', + freezeTableName: true + }) + + // bind to `this` + this.sequelize = sequelize + this.Framework = Framework + this.FrameworkHistory = FrameworkHistory + this.Pod = Pod + this.FrameworkEvent = FrameworkEvent + this.PodEvent = PodEvent + this.Version = Version + this.synchronizeSchema = this.synchronizeSchema.bind(this) + } + + async synchronizeSchema (force = false) { + if (force === true) { + await this.sequelize.sync({ force: true }) + } else { + await Promise.all([ + this.Framework.sync({ alter: true }), + this.FrameworkHistory.sync({ alter: true }), + this.Pod.sync({ alter: true }), + this.FrameworkEvent.sync({ alter: true }), + this.PodEvent.sync({ alter: true }), + this.Version.sync({ alter: true }) + ]) + } + } + + async ping () { + await this.sequelize.authenticate() + } + + async getVersion () { + const res = await this.Version.findOne() + if (res) { + return { + version: res.version, + commitVersion: res.commitVersion + } + } else { + return { + version: null, + commitVersion: null + } + } + } + + async setVersion (version, commitVersion) { + await this.sequelize.transaction(async (t) => { + await this.Version.destroy({ + where: {}, transaction: t + }) + await this.Version.create({ + version: version, + commitVersion: commitVersion + }, { transaction: t }) + }) + } +} + +module.exports = DatabaseModel diff --git a/src/database-controller/sdk/package.json b/src/database-controller/sdk/package.json new file mode 100644 index 0000000000..70c84a3138 --- /dev/null +++ b/src/database-controller/sdk/package.json @@ -0,0 +1,14 @@ +{ + "name": "openpaidbsdk", + "version": "1.0.0", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "standard.cmd --fix" + }, + "main": "index.js", + "license": "MIT", + "dependencies": { + "pg": "^8.2.1", + "sequelize": "5.21.3" + } +} diff --git a/src/database-controller/sdk/yarn.lock b/src/database-controller/sdk/yarn.lock new file mode 100644 index 0000000000..dd25fd79d1 --- /dev/null +++ b/src/database-controller/sdk/yarn.lock @@ -0,0 +1,241 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "14.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" + integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== + +any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +cls-bluebird@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cls-bluebird/-/cls-bluebird-2.1.0.tgz#37ef1e080a8ffb55c2f4164f536f1919e7968aee" + integrity sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4= + dependencies: + is-bluebird "^1.0.2" + shimmer "^1.1.0" + +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +dottie@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.2.tgz#cc91c0726ce3a054ebf11c55fbc92a7f266dd154" + integrity sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg== + +inflection@1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= + +is-bluebird@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2" + integrity sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI= + +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +moment-timezone@^0.5.21: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.24.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +pg-connection-string@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.2.3.tgz#48e1158ec37eaa82e98dbcb7307103ec303fe0e7" + integrity sha512-I/KCSQGmOrZx6sMHXkOs2MjddrYcqpza3Dtsy0AjIgBr/bZiPJRK9WhABXN1Uy1UDazRbi9gZEzO2sAhL5EqiQ== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0" + integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA== + +pg-protocol@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.2.4.tgz#3139cac0e51347f1e21e03954b1bb9fe2c20962e" + integrity sha512-/8L/G+vW/VhWjTGXpGh8XVkXOFx1ZDY+Yuz//Ab8CfjInzFkreI+fDG3WjCeSra7fIZwAFxzbGptNbm8xSXenw== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.2.1.tgz#f5a81f5e2025182fbe701514d3e1a43e68a616ac" + integrity sha512-DKzffhpkWRr9jx7vKxA+ur79KG+SKw+PdjMb1IRhMiKI9zqYUGczwFprqy+5Veh/DCcFs1Y6V8lRLN5I1DlleQ== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.2.3" + pg-pool "^3.2.1" + pg-protocol "^1.2.4" + pg-types "^2.1.0" + pgpass "1.x" + semver "4.3.2" + +pgpass@1.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" + integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= + dependencies: + split "^1.0.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.5.tgz#710b27de5f27d550f6e80b5d34f7ba189213c2ee" + integrity sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +retry-as-promised@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543" + integrity sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg== + dependencies: + any-promise "^1.3.0" + +semver@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" + integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +sequelize-pool@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.3.0.tgz#64f1fe8744228172c474f530604b6133be64993d" + integrity sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA== + +sequelize@5.21.3: + version "5.21.3" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.21.3.tgz#f8a6fa0245f8995d70849e4da00c2c7c9aa9f569" + integrity sha512-ptdeAxwTY0zbj7AK8m+SH3z52uHVrt/qmOTSIGo/kyfnSp3h5HeKlywkJf5GEk09kuRrPHfWARVSXH1W3IGU7g== + dependencies: + bluebird "^3.5.0" + cls-bluebird "^2.1.0" + debug "^4.1.1" + dottie "^2.0.0" + inflection "1.12.0" + lodash "^4.17.15" + moment "^2.24.0" + moment-timezone "^0.5.21" + retry-as-promised "^3.2.0" + semver "^6.3.0" + sequelize-pool "^2.3.0" + toposort-class "^1.0.1" + uuid "^3.3.3" + validator "^10.11.0" + wkx "^0.4.8" + +shimmer@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +through@2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= + +uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validator@^10.11.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" + integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== + +wkx@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.8.tgz#a092cf088d112683fdc7182fd31493b2c5820003" + integrity sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ== + dependencies: + "@types/node" "*" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== diff --git a/src/database-controller/src/.dockerignore b/src/database-controller/src/.dockerignore new file mode 100644 index 0000000000..1231151c1d --- /dev/null +++ b/src/database-controller/src/.dockerignore @@ -0,0 +1,63 @@ +.git + +# Directory for submitted jobs' json file and scripts +frameworklauncher/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env \ No newline at end of file diff --git a/src/database-controller/src/core/config.js b/src/database-controller/src/core/config.js new file mode 100644 index 0000000000..b90fda23eb --- /dev/null +++ b/src/database-controller/src/core/config.js @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const Joi = require('joi') + +const configSchema = Joi.object().keys({ + logLevel: Joi.string() + .valid('error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly') + .default('info'), + k8sConnectionTimeoutSecond: Joi.number() + .integer() + .required(), + writeMergerConnectionTimeoutSecond: Joi.number() + .integer() + .required(), + customK8sApiServerURL: Joi.string() + .uri() + .optional(), + customK8sCaFile: Joi.string() + .optional(), + customK8sTokenFile: Joi.string() + .optional(), + recoveryModeEnabled: Joi.boolean() + .required() +}).required() + +const config = { + logLevel: process.env.LOG_LEVEL, + k8sConnectionTimeoutSecond: parseInt(process.env.K8S_CONNECTION_TIMEOUT_SECOND), + writeMergerConnectionTimeoutSecond: parseInt(process.env.WRITE_MERGER_CONNECTION_TIMEOUT_SECOND), + customK8sApiServerURL: process.env.CUSTOM_K8S_API_SERVER_URL, + customK8sCaFile: process.env.CUSTOM_K8S_CA_FILE, + customK8sTokenFile: process.env.CUSTOM_K8S_TOKEN_FILE, + recoveryModeEnabled: process.env.RECOVERY_MODE_ENABLED === 'true' +} + +const { error, value } = Joi.validate(config, configSchema) +if (error) { + throw new Error(`Config error\n${error}`) +} + +module.exports = value diff --git a/src/database-controller/src/core/framework.js b/src/database-controller/src/core/framework.js new file mode 100644 index 0000000000..c784cb0272 --- /dev/null +++ b/src/database-controller/src/core/framework.js @@ -0,0 +1,372 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const logger = require('@dbc/core/logger') +const k8s = require('@dbc/core/k8s') +const _ = require('lodash') +const yaml = require('js-yaml') + +const mockFrameworkStatus = () => { + return { + state: 'AttemptCreationPending', + attemptStatus: { + completionStatus: null, + taskRoleStatuses: [] + }, + retryPolicyStatus: { + retryDelaySec: null, + totalRetriedCount: 0, + accountableRetriedCount: 0 + } + } +} + +const convertState = (state, exitCode, retryDelaySec) => { + switch (state) { + case 'AttemptCreationPending': + case 'AttemptCreationRequested': + case 'AttemptPreparing': + return 'WAITING' + case 'AttemptRunning': + return 'RUNNING' + case 'AttemptDeletionPending': + case 'AttemptDeletionRequested': + case 'AttemptDeleting': + if (exitCode === -210 || exitCode === -220) { + return 'STOPPING' + } else { + return 'RUNNING' + } + case 'AttemptCompleted': + if (retryDelaySec == null) { + return 'RUNNING' + } else { + return 'WAITING' + } + case 'Completed': + if (exitCode === 0) { + return 'SUCCEEDED' + } else if (exitCode === -210 || exitCode === -220) { + return 'STOPPED' + } else { + return 'FAILED' + } + default: + return 'UNKNOWN' + } +} + +function ignoreError (err) { + logger.info('This error will be ignored: ', err) +} + +class Snapshot { + constructor (snapshot) { + if (snapshot instanceof Object) { + this._snapshot = _.cloneDeep(snapshot) + } else { + this._snapshot = JSON.parse(snapshot) + } + if (!this._snapshot.status) { + this._snapshot.status = mockFrameworkStatus() + } + } + + copy () { + return new Snapshot(this._snapshot) + } + + getRequest (omitGeneration) { + const request = _.pick(this._snapshot, [ + 'apiVersion', + 'kind', + 'metadata.name', + 'metadata.labels', + 'metadata.annotations', + 'spec' + ]) + if (omitGeneration) { + return _.omit(request, 'metadata.annotations.requestGeneration') + } else { + return request + } + } + + overrideRequest (otherSnapshot) { + // shouldn't use _.merge here + _.assign(this._snapshot, _.pick(otherSnapshot._snapshot, [ + 'apiVersion', + 'kind', + 'spec' + ])) + _.assign(this._snapshot.metadata, _.pick(otherSnapshot._snapshot.metadata, [ + 'name', + 'labels', + 'annotations' + ])) + } + + getRequestUpdate (withSnapshot = true) { + const loadedConfig = yaml.safeLoad(this._snapshot.metadata.annotations.config) + const jobPriority = _.get(loadedConfig, 'extras.hivedscheduler.jobPriorityClass', null) + const update = { + name: this._snapshot.metadata.name, + namespace: this._snapshot.metadata.namespace, + jobName: this._snapshot.metadata.annotations.jobName, + userName: this._snapshot.metadata.labels.userName, + jobConfig: this._snapshot.metadata.annotations.config, + executionType: this._snapshot.spec.executionType, + virtualCluster: this._snapshot.metadata.labels.virtualCluster, + jobPriority: jobPriority, + totalGpuNumber: this._snapshot.metadata.annotations.totalGpuNumber, + totalTaskNumber: this._snapshot.spec.taskRoles.reduce((num, spec) => num + spec.taskNumber, 0), + totalTaskRoleNumber: this._snapshot.spec.taskRoles.length, + logPathInfix: this._snapshot.metadata.annotations.logPathInfix + } + if (withSnapshot) { + update.snapshot = JSON.stringify(this._snapshot) + } + return update + } + + getStatusUpdate (withSnapshot = true) { + const completionStatus = this._snapshot.status.attemptStatus.completionStatus + const update = { + retries: this._snapshot.status.retryPolicyStatus.totalRetriedCount, + retryDelayTime: this._snapshot.status.retryPolicyStatus.retryDelaySec, + platformRetries: this._snapshot.status.retryPolicyStatus.totalRetriedCount - this._snapshot.status.retryPolicyStatus.accountableRetriedCount, + resourceRetries: 0, + userRetries: this._snapshot.status.retryPolicyStatus.accountableRetriedCount, + creationTime: this._snapshot.metadata.creationTimestamp ? new Date(this._snapshot.metadata.creationTimestamp) : null, + completionTime: this._snapshot.status.completionTime ? new Date(this._snapshot.status.completionTime) : null, + appExitCode: completionStatus ? completionStatus.code : null, + subState: this._snapshot.status.state, + state: convertState( + this._snapshot.status.state, + completionStatus ? completionStatus.code : null, + this._snapshot.status.retryPolicyStatus.retryDelaySec + ) + } + if (withSnapshot) { + update.snapshot = JSON.stringify(this._snapshot) + } + return update + } + + getAllUpdate (withSnapshot = true) { + const update = _.assign({}, this.getRequestUpdate(false), this.getStatusUpdate(false)) + if (withSnapshot) { + update.snapshot = JSON.stringify(this._snapshot) + } + return update + } + + getRecordForLegacyTransfer () { + const record = this.getAllUpdate() + // correct submissionTime is lost, use snapshot.metadata.creationTimestamp instead + if (this.hasCreationTime()) { + record.submissionTime = this.getCreationTime() + } else { + record.submissionTime = new Date() + } + this.setGeneration(1) + return record + } + + getName () { + return this._snapshot.metadata.name + } + + getSnapshot () { + return _.cloneDeep(this._snapshot) + } + + getString () { + return JSON.stringify(this._snapshot) + } + + hasCreationTime () { + if (_.get(this._snapshot, 'metadata.creationTimestamp')) { + return true + } else { + return false + } + } + + getCreationTime () { + if (this.hasCreationTime()) { + return new Date(this._snapshot.metadata.creationTimestamp) + } else { + return null + } + } + + setGeneration (generation) { + this._snapshot.metadata.annotations.requestGeneration = (generation).toString() + } + + getGeneration () { + if (!_.has(this._snapshot, 'metadata.annotations.requestGeneration')) { + // for some legacy jobs, use 1 as its request generation. + this.setGeneration(1) + } + return parseInt(this._snapshot.metadata.annotations.requestGeneration) + } +} + +class AddOns { + constructor (configSecretDef = null, priorityClassDef = null, dockerSecretDef = null) { + if (configSecretDef !== null && !(configSecretDef instanceof Object)) { + this._configSecretDef = JSON.parse(configSecretDef) + } else { + this._configSecretDef = configSecretDef + } + if (priorityClassDef !== null && !(priorityClassDef instanceof Object)) { + this._priorityClassDef = JSON.parse(priorityClassDef) + } else { + this._priorityClassDef = priorityClassDef + } + if (dockerSecretDef !== null && !(dockerSecretDef instanceof Object)) { + this._dockerSecretDef = JSON.parse(dockerSecretDef) + } else { + this._dockerSecretDef = dockerSecretDef + } + } + + async create () { + if (this._configSecretDef) { + try { + await k8s.createSecret(this._configSecretDef) + } catch (err) { + if (err.response && err.response.statusCode === 409) { + logger.warn(`Secret ${this._configSecretDef.metadata.name} already exists.`) + } else { + throw err + } + } + } + if (this._priorityClassDef) { + try { + await k8s.createPriorityClass(this._priorityClassDef) + } catch (err) { + if (err.response && err.response.statusCode === 409) { + logger.warn(`PriorityClass ${this._priorityClassDef.metadata.name} already exists.`) + } else { + throw err + } + } + } + if (this._dockerSecretDef) { + try { + await k8s.createSecret(this._dockerSecretDef) + } catch (err) { + if (err.response && err.response.statusCode === 409) { + logger.warn(`Secret ${this._dockerSecretDef.metadata.name} already exists.`) + } else { + throw err + } + } + } + } + + silentPatch (frameworkResponse) { + // do not await for patch + this._configSecretDef && k8s.patchSecretOwnerToFramework(this._configSecretDef, frameworkResponse).catch(ignoreError) + this._dockerSecretDef && k8s.patchSecretOwnerToFramework(this._dockerSecretDef, frameworkResponse).catch(ignoreError) + } + + silentDelete () { + // do not await for delete + this._configSecretDef && k8s.deleteSecret(this._configSecretDef.metadata.name).catch(ignoreError) + this._priorityClassDef && k8s.deletePriorityClass(this._priorityClassDef.metadata.name).catch(ignoreError) + this._dockerSecretDef && k8s.deleteSecret(this._dockerSecretDef.metadata.name).catch(ignoreError) + } + + getUpdate () { + const update = {} + if (this._configSecretDef) { + update.configSecretDef = JSON.stringify(this._configSecretDef) + } + if (this._priorityClassDef) { + update.priorityClassDef = JSON.stringify(this._priorityClassDef) + } + if (this._dockerSecretDef) { + update.dockerSecretDef = JSON.stringify(this._dockerSecretDef) + } + return update + } +} + +async function synchronizeCreate (snapshot, addOns) { + await addOns.create() + try { + const response = await k8s.createFramework(snapshot.getRequest(false)) + // framework is created successfully. + const frameworkResponse = response.body + addOns.silentPatch(frameworkResponse) + return frameworkResponse + } catch (err) { + if (err.response && err.response.statusCode === 409) { + // doesn't delete add-ons if 409 error + logger.warn(`Framework ${snapshot.getName()} already exists.`) + throw err + } else { + // delete add-ons if 409 error + addOns.silentDelete() + throw err + } + } +} + +async function synchronizeModify (snapshot) { + const response = await k8s.patchFramework(snapshot.getName(), snapshot.getRequest(false)) + const frameworkResponse = response.body + return frameworkResponse +} + +async function synchronizeRequest (snapshot, addOns) { + // any error will be raised + // if succeed, return framework from api server + // There may be multiple calls of synchronizeRequest. + try { + await k8s.getFramework(snapshot.getName()) + // if framework exists + const frameworkResponse = await synchronizeModify(snapshot) + logger.info(`Request of framework ${snapshot.getName()} is successfully patched.`) + return frameworkResponse + } catch (err) { + if (err.response && err.response.statusCode === 404) { + const frameworkResponse = await synchronizeCreate(snapshot, addOns) + logger.info(`Request of framework ${snapshot.getName()} is successfully created.`) + return frameworkResponse + } else { + throw err + } + } +} + +function silentSynchronizeRequest (snapshot, addOns) { + // any error will be ignored + synchronizeRequest(snapshot, addOns).catch(ignoreError) +} + +module.exports = { + Snapshot, + AddOns, + synchronizeRequest, + silentSynchronizeRequest +} diff --git a/src/database-controller/src/core/k8s.js b/src/database-controller/src/core/k8s.js new file mode 100644 index 0000000000..3ca7f4091a --- /dev/null +++ b/src/database-controller/src/core/k8s.js @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const k8s = require('@kubernetes/client-node') +const logger = require('./logger') +const config = require('./config') +const { timeoutDecorator } = require('./util') +const kc = new k8s.KubeConfig() + +if (config.customK8sApiServerURL) { + // For local debugging, one should set CUSTOM_K8S_API_SERVER_URL, CUSTOM_K8S_CA_FILE and CUSTOM_K8S_TOKEN_FILE. + const cluster = { + name: 'inCluster', + caFile: config.customK8sCaFile, + server: config.customK8sApiServerURL, + skipTLSVerify: false + } + const user = { + name: 'inClusterUser', + authProvider: + { + name: 'tokenFile', + config: + { + tokenFile: config.customK8sTokenFile + } + } + } + kc.loadFromClusterAndUser(cluster, user) +} else { + // For in-cluster containers, load KubeConfig from default setting. + kc.loadFromDefault() +} + +// If api server reports a non-200 status code, the client will +// throw an error. In such case, error.name will be "HttpError", +// and error.response.statusCode is the actual status code. +// If network is disconnected, the error will be a different one, +// and you cannot get error.name and error.statusCode. +const customObjectsClient = kc.makeApiClient(k8s.CustomObjectsApi) + +async function getFramework (name, namespace = 'default') { + const res = await customObjectsClient.getNamespacedCustomObject( + 'frameworkcontroller.microsoft.com', + 'v1', + namespace, + 'frameworks', + name + ) + return res.response +} + +async function listFramework (name, namespace = 'default') { + const res = await customObjectsClient.listNamespacedCustomObject( + 'frameworkcontroller.microsoft.com', + 'v1', + namespace, + 'frameworks' + ) + return res.response +} + +async function createFramework (frameworkDescription, namespace = 'default') { + const res = await customObjectsClient.createNamespacedCustomObject( + 'frameworkcontroller.microsoft.com', + 'v1', + namespace, + 'frameworks', + frameworkDescription + ) + return res.response +} + +async function patchFramework (name, data, namespace = 'default') { + const res = await customObjectsClient.patchNamespacedCustomObject( + 'frameworkcontroller.microsoft.com', + 'v1', + namespace, + 'frameworks', + name, + data, + { headers: { 'Content-Type': 'application/merge-patch+json' } } + ) + return res.response +} + +async function deleteFramework (name, namespace = 'default') { + const res = await customObjectsClient.deleteNamespacedCustomObject( + 'frameworkcontroller.microsoft.com', + 'v1', + namespace, + 'frameworks', + name + ) + return res.response +} + +function getFrameworkInformer (timeoutSeconds = 365 * 86400, namespace = 'default') { + /* + Usage: + + const informer = getFrameworkInformer() + informer.on('add', (obj) => { console.log(`Added: ${obj.metadata.name}`); }); + informer.on('update', (obj) => { console.log(`Updated: ${obj.metadata.name}`); }); + informer.on('delete', (obj) => { console.log(`Deleted: ${obj.metadata.name}`); }); + informer.on('error', (err) => { console.error(err);}); + informer.start(); + + If the informer disconnects normally from API server, it will re-connect automatically. + But during the reconnection, listFn will be called again, which is inefficient. + According to https://github.com/kubernetes-client/javascript/blob/932c2fbc34db954c6ed397b3cd9ead08b2ff1d10/src/cache.ts#L82-L85, + this behavior will be fixed in the future. + TO DO: If @kubernetes/client-node fixes this issue, we should upgrade our code to use the new code. + + If the informer encounters any error, it will stop watching, and won't re-connect. + One can restart it by informer.on('error', (err) => { informer.start(); }). + + */ + const listFn = () => { + logger.info('Frameworks are listed.') + return customObjectsClient.listNamespacedCustomObject( + 'frameworkcontroller.microsoft.com', + 'v1', + namespace, + 'frameworks' + ) + } + const informer = k8s.makeInformer( + kc, + `/apis/frameworkcontroller.microsoft.com/v1/frameworks?timeoutSeconds=${timeoutSeconds}`, + listFn + ) + return informer +} + +const priorityClassClient = kc.makeApiClient(k8s.SchedulingV1Api) + +async function createPriorityClass (priorityClassDef) { + const res = await priorityClassClient.createPriorityClass(priorityClassDef) + return res.response +} + +async function deletePriorityClass (name) { + const res = await priorityClassClient.deletePriorityClass(name) + return res.response +} + +const coreV1Client = kc.makeApiClient(k8s.CoreV1Api) + +async function createSecret (secretDef) { + const res = await coreV1Client.createNamespacedSecret(secretDef.metadata.namespace, secretDef) + return res +} + +async function deleteSecret (name, namespace = 'default') { + const res = await coreV1Client.deleteNamespacedSecret(name, namespace) + return res.response +} + +async function patchSecretOwnerToFramework (secret, frameworkResponse) { + const metadata = { + ownerReferences: [{ + apiVersion: 'frameworkcontroller.microsoft.com', + kind: 'Framework', + name: frameworkResponse.metadata.name, + uid: frameworkResponse.metadata.uid, + controller: true, + blockOwnerDeletion: true + }] + } + const res = await coreV1Client.patchNamespacedSecret( + secret.metadata.name, secret.metadata.namespace, + { metadata: metadata }, + ...Array(4), // skip some parameters + { headers: { 'Content-Type': 'application/merge-patch+json' } }) + return res.response +} + +const timeoutMs = config.k8sConnectionTimeoutSecond * 1000 + +module.exports = { + getFramework: timeoutDecorator(getFramework, 'Kubernetes getFramework', timeoutMs), + listFramework: timeoutDecorator(listFramework, 'Kubernetes getFramework', timeoutMs), + createFramework: timeoutDecorator(createFramework, 'Kubernetes createFramework', timeoutMs), + patchFramework: timeoutDecorator(patchFramework, 'Kubernetes patchFramework', timeoutMs), + deleteFramework: timeoutDecorator(deleteFramework, 'Kubernetes deleteFramework', timeoutMs), + createPriorityClass: timeoutDecorator(createPriorityClass, 'Kubernetes createPriorityClass', timeoutMs), + deletePriorityClass: timeoutDecorator(deletePriorityClass, 'Kubernetes deletePriorityClass', timeoutMs), + createSecret: timeoutDecorator(createSecret, 'Kubernetes createSecret', timeoutMs), + deleteSecret: timeoutDecorator(deleteSecret, 'Kubernetes deleteSecret', timeoutMs), + patchSecretOwnerToFramework: timeoutDecorator(patchSecretOwnerToFramework, 'Kubernetes patchSecretOwnerToFramework', timeoutMs), + getFrameworkInformer: getFrameworkInformer +} diff --git a/src/database-controller/src/core/logger.js b/src/database-controller/src/core/logger.js new file mode 100644 index 0000000000..60289004ce --- /dev/null +++ b/src/database-controller/src/core/logger.js @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const util = require('util') +const winston = require('winston') +const config = require('./config') + +const logTransports = { + console: new winston.transports.Console({ + json: false, + colorize: true, + timestamp: () => new Date().toISOString(), + formatter: (options) => { + const timestamp = options.timestamp() + const level = winston.config.colorize( + options.level, + options.level.toUpperCase() + ) + const message = options.message ? options.message : '' + const meta = options.meta && Object.keys(options.meta).length + ? '\nmeta = ' + JSON.stringify(options.meta, null, 2) : '' + return util.format(timestamp, '[' + level + ']', message, meta) + } + }) +} + +// create logger +const logger = new winston.Logger({ + level: config.logLevel, + transports: [ + logTransports.console + ], + exitOnError: false +}) + +logger.stream = { + write: (message, encoding) => { + logger.info(message.trim()) + } +} + +// module exports +module.exports = logger diff --git a/src/database-controller/src/core/util.js b/src/database-controller/src/core/util.js new file mode 100644 index 0000000000..d637d45e19 --- /dev/null +++ b/src/database-controller/src/core/util.js @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const logger = require('@dbc/core/logger') + +async function timePeriod (ms) { + await new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms) + }) +} + +function alwaysRetryDecorator (promiseFn, loggingMessage, initialRetryDelayMs = 500, backoffRatio = 2, maxRetryDelayMs = 120000) { + /* + promiseFn is an async function + This decorator returns a newPromiseFn, which can be run as newPromiseFn(...). + The new promise will be always retried. + */ + async function _wrapper () { + let nextDelayMs = initialRetryDelayMs + let retryCount = 0 + while (true) { + try { + if (retryCount > 0) { + logger.warn(`${loggingMessage} retries=${retryCount}.`) + } + const res = await promiseFn.apply(this, arguments) + logger.info(`${loggingMessage} succeeded.`) + return res + } catch (err) { + if (retryCount === 0) { + logger.warn(`${loggingMessage} failed. It will be retried after ${nextDelayMs} ms. Error: ${err.message}`) + } else { + logger.warn(`${loggingMessage} failed. Retries=${retryCount}. It will be retried after ${nextDelayMs} ms. Error: ${err.message}`) + } + await timePeriod(nextDelayMs) + if (nextDelayMs * backoffRatio < maxRetryDelayMs) { + nextDelayMs = nextDelayMs * backoffRatio + } else { + nextDelayMs = maxRetryDelayMs + } + retryCount += 1 + } + } + } + return _wrapper +} + +function timeoutDecorator (promiseFn, loggingMessage, timeoutMs) { + /* + promiseFn is an async function + This decorator returns a newPromiseFn, which can be run as newPromiseFn(...). + The new promise will has a timeout + */ + async function _wrapper () { + const timeoutPromise = new Promise((resolve, reject) => { + setTimeout(() => reject(new Error(`${loggingMessage} reached timeout ${timeoutMs} ms.`)), timeoutMs) + }) + const resPromise = promiseFn.apply(this, arguments) + const res = await Promise.race([timeoutPromise, resPromise]) + return res + } + return _wrapper +} + +module.exports = { + alwaysRetryDecorator: alwaysRetryDecorator, + timeoutDecorator: timeoutDecorator +} diff --git a/src/database-controller/src/index.js b/src/database-controller/src/index.js new file mode 100644 index 0000000000..a175ab6e09 --- /dev/null +++ b/src/database-controller/src/index.js @@ -0,0 +1,23 @@ +require('module-alias/register') +require('dotenv').config() +const k8s = require('@dbc/core/k8s') +const logger = require('@dbc/core/logger') + +async function main () { + let res + res = await k8s.createSecret( + 'test', + { 'test-key': Buffer.from('test-value').toString('base64') } + ) + logger.info(res) + res = await k8s.patchSecretOwnerToFramework( + 'test', + 'e76bcfa99de735b3e16c2470f5e7ca2b', + '865fc53f-50f8-4c25-9a62-18bab47063aa' + ) + logger.info(res) + res = await k8s.deleteSecret('test') + logger.info(res) +} + +main() diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js new file mode 100644 index 0000000000..59fc7d1aef --- /dev/null +++ b/src/database-controller/src/initializer/index.js @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +require('module-alias/register') +require('dotenv').config() +const DatabaseModel = require('openpaidbsdk') +const fs = require('fs') +const logger = require('@dbc/core/logger') +const neverResolved = new Promise((resolve, reject) => {}) +const { paiVersion, paiCommitVersion } = require('@dbc/package.json') +const k8s = require('@dbc/core/k8s') +const { Snapshot } = require('@dbc/core/framework') + +async function updateFromNoDatabaseVersion (databaseModel) { + // update from 1.0.0 < version < v1.2.0 + await databaseModel.synchronizeSchema() + // transfer old frameworks from api server to db + const frameworks = (await k8s.listFramework()).body.items + console.log(frameworks) + for (const framework of frameworks) { + const snapshot = new Snapshot(framework) + logger.info(`Transferring framework ${snapshot.getName()} to database.`) + const record = snapshot.getRecordForLegacyTransfer() + record.requestSynced = true + await databaseModel.Framework.upsert(record) + } + // TO DO: transfer old framework history from api server to db +} + +async function main () { + try { + const databaseModel = new DatabaseModel( + process.env.DB_CONNECTION_STR, + 1 + ) + const previousVersion = (await databaseModel.getVersion()).version + if (!previousVersion) { + await updateFromNoDatabaseVersion(databaseModel) + } + await databaseModel.setVersion(paiVersion, paiCommitVersion) + await new Promise((resolve, reject) => { + fs.writeFile('/READY', '', (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + logger.info('Database has been successfully initialized.') + } catch (err) { + logger.error(err) + } + await neverResolved // sleep forever +} + +main() diff --git a/src/database-controller/src/package.json b/src/database-controller/src/package.json new file mode 100644 index 0000000000..f80c88a6d2 --- /dev/null +++ b/src/database-controller/src/package.json @@ -0,0 +1,35 @@ +{ + "name": "database-controller", + "version": "1.0.0", + "paiVersion": null, + "paiCommitVersion": null, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "standard.cmd --fix" + }, + "main": "index.js", + "license": "MIT", + "dependencies": { + "@kubernetes/client-node": "0.11.2", + "async-lock": "^1.2.4", + "body-parser": "^1.19.0", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^8.2.0", + "express": "^4.17.1", + "http-errors": "^1.8.0", + "interval-promise": "^1.4.0", + "joi": "^14.3.1", + "js-yaml": "^3.14.0", + "lodash": "^4.17.19", + "module-alias": "^2.2.2", + "morgan": "^1.10.0", + "node-fetch": "^2.6.0", + "openpaidbsdk": "file:../sdk", + "statuses": "^2.0.0", + "winston": "2" + }, + "_moduleAliases": { + "@dbc": "." + } +} diff --git a/src/database-controller/src/poller/config.js b/src/database-controller/src/poller/config.js new file mode 100644 index 0000000000..c95eb1d4eb --- /dev/null +++ b/src/database-controller/src/poller/config.js @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const basicConfig = require('@dbc/core/config') +const _ = require('lodash') +const Joi = require('joi') + +const configSchema = Joi.object().keys({ + dbConnectionStr: Joi.string() + .required(), + maxDatabaseConnection: Joi.number() + .integer() + .required(), + intervalSecond: Joi.number() + .integer() + .required(), + writeMergerUrl: Joi.string() + .uri() + .required() +}).required() + +const config = { + dbConnectionStr: process.env.DB_CONNECTION_STR, + maxDatabaseConnection: parseInt(process.env.MAX_DB_CONNECTION), + intervalSecond: parseInt(process.env.INTERVAL_SECOND), + writeMergerUrl: process.env.WRITE_MERGER_URL +} + +const { error, value } = Joi.validate(config, configSchema) +if (error) { + throw new Error(`Config error\n${error}`) +} + +module.exports = _.assign(basicConfig, value) diff --git a/src/database-controller/src/poller/index.js b/src/database-controller/src/poller/index.js new file mode 100644 index 0000000000..7a6ff016f8 --- /dev/null +++ b/src/database-controller/src/poller/index.js @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +require('module-alias/register') +require('dotenv').config() +const AsyncLock = require('async-lock') +const { Sequelize } = require('sequelize') +const Op = Sequelize.Op +const DatabaseModel = require('openpaidbsdk') +const logger = require('@dbc/core/logger') +const { Snapshot, AddOns, synchronizeRequest } = require('@dbc/core/framework') +const interval = require('interval-promise') +const config = require('@dbc/poller/config') +const fetch = require('node-fetch') +const { deleteFramework } = require('@dbc/core/k8s') +const lock = new AsyncLock({ maxPending: 1 }) +const databaseModel = new DatabaseModel( + config.dbConnectionStr, + config.maxDatabaseConnection +) + +async function mockDeleteEvent (snapshot) { + await fetch( + `${config.writeMergerUrl}/api/v1/watchEvents/DELETED`, + { + method: 'POST', + body: snapshot.getString(), + headers: { 'Content-Type': 'application/json' }, + timeout: config.writeMergerConnectionTimeoutSecond * 1000 + } + ) +} + +function deleteHandler (snapshot, pollingTs) { + const frameworkName = snapshot.getName() + logger.info(`Will delete framework ${frameworkName}. PollingTs=${pollingTs}.`) + lock.acquire(frameworkName, + async () => { + try { + await deleteFramework(snapshot.getName()) + logger.info(`Framework ${frameworkName} is successfully deleted. PollingTs=${pollingTs}.`) + } catch (err) { + if (err.response && err.response.statusCode === 404) { + // for 404 error, mock a delete to write merger + logger.warn(`Cannot find framework ${frameworkName} in API Server. Will mock a deletion to write merger.`) + await mockDeleteEvent(snapshot) + } else { + // for non-404 error + throw err + } + } + } + ).catch((err) => { + logger.error( + `An error happened when delete framework ${frameworkName} and pollingTs=${pollingTs}:`, + err + ) + }) +} + +function synchronizeHandler (snapshot, addOns, pollingTs) { + const frameworkName = snapshot.getName() + logger.info(`Start synchronizing request of framework ${frameworkName}. PollingTs=${pollingTs}`) + lock.acquire( + frameworkName, + async () => { + await synchronizeRequest( + snapshot, + addOns + ) + logger.info(`Request of framework ${frameworkName} is successfully synchronized. PollingTs=${pollingTs}.`) + } + ).catch((err) => { + logger.error( + `An error happened when synchronize request for framework ${frameworkName} and pollingTs=${pollingTs}:` + , err + ) + }) +} + +async function poll () { + const pollingTs = new Date().getTime() + try { + logger.info(`Start polling. PollingTs=${pollingTs}`) + const frameworks = await databaseModel.Framework.findAll({ + attributes: ['name', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef', 'snapshot', + 'subState', 'requestSynced', 'apiServerDeleted'], + where: { + apiServerDeleted: false, + [Op.or]: { + subState: 'Completed', + requestSynced: false + } + } + }) + for (const framework of frameworks) { + const snapshot = new Snapshot(framework.snapshot) + const addOns = new AddOns(framework.configSecretDef, framework.priorityClassDef, framework.dockerSecretDef) + if (framework.subState === 'Completed') { + deleteHandler(snapshot, pollingTs) + } else { + synchronizeHandler(snapshot, addOns, pollingTs) + } + } + } catch (err) { + logger.error(`An error happened for pollingTs=${pollingTs}:`, err) + throw err + } +} + +interval( + poll, + config.intervalSecond * 1000, + { stopOnError: false } +) diff --git a/src/database-controller/src/watcher/framework/config.js b/src/database-controller/src/watcher/framework/config.js new file mode 100644 index 0000000000..4a4098abf0 --- /dev/null +++ b/src/database-controller/src/watcher/framework/config.js @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const basicConfig = require('@dbc/core/config') +const _ = require('lodash') +const Joi = require('joi') + +const configSchema = Joi.object().keys({ + writeMergerUrl: Joi.string() + .uri() + .required() +}).required() + +const config = { + writeMergerUrl: process.env.WRITE_MERGER_URL +} + +const { error, value } = Joi.validate(config, configSchema) +if (error) { + throw new Error(`Config error\n${error}`) +} + +module.exports = _.assign(basicConfig, value) diff --git a/src/database-controller/src/watcher/framework/index.js b/src/database-controller/src/watcher/framework/index.js new file mode 100644 index 0000000000..30d08c18ca --- /dev/null +++ b/src/database-controller/src/watcher/framework/index.js @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +require('module-alias/register') +require('dotenv').config() +const fetch = require('node-fetch') +const AsyncLock = require('async-lock') +const logger = require('@dbc/core/logger') +const { getFrameworkInformer } = require('@dbc/core/k8s') +const { alwaysRetryDecorator } = require('@dbc/core/util') +const config = require('@dbc/watcher/framework/config') + +const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }) + +async function synchronizeFramework (eventType, apiObject) { + const res = await fetch( + `${config.writeMergerUrl}/api/v1/watchEvents/${eventType}`, + { + method: 'POST', + body: JSON.stringify(apiObject), + headers: { 'Content-Type': 'application/json' }, + timeout: config.writeMergerConnectionTimeoutSecond * 1000 + } + ) + if (!res.ok) { + throw new Error(`Request returns a ${res.status} error.`) + } +} + +const eventHandler = (eventType, apiObject) => { + /* + framework name-based lock + always retry + */ + const receivedTs = (new Date()).getTime() + const state = (apiObject.status && apiObject.status.state) ? apiObject.status.state : 'Unknown' + logger.info(`Event type=${eventType} receivedTs=${receivedTs} framework=${apiObject.metadata.name} state=${state} received.`) + lock.acquire( + apiObject.metadata.name, + alwaysRetryDecorator( + () => synchronizeFramework(eventType, apiObject), + `Sync to write merger type=${eventType} receivedTs=${receivedTs} framework=${apiObject.metadata.name} state=${state}` + ) + ) +} + +const informer = getFrameworkInformer() + +informer.on('add', (apiObject) => { eventHandler('ADDED', apiObject) }) +informer.on('update', (apiObject) => { eventHandler('MODIFED', apiObject) }) +informer.on('delete', (apiObject) => { eventHandler('DELETED', apiObject) }) +informer.on('error', (err) => { + logger.error(err) + process.exit(1) +}) +informer.start() diff --git a/src/database-controller/src/write-merger/app.js b/src/database-controller/src/write-merger/app.js new file mode 100644 index 0000000000..dd103bd0ad --- /dev/null +++ b/src/database-controller/src/write-merger/app.js @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const cors = require('cors') +const morgan = require('morgan') +const express = require('express') +const compress = require('compression') +const bodyParser = require('body-parser') +const logger = require('@dbc/core/logger') +const status = require('statuses') +const handler = require('@dbc/write-merger/handler') +const config = require('@dbc/write-merger/config') + +const app = express() + +app.use(cors()) +app.use(compress()) +app.use(bodyParser.urlencoded({ extended: true })) +app.use(bodyParser.json({ limit: config.bodyLimit })) +app.use(bodyParser.text({ type: 'text/*' })) +app.use(morgan('dev', { stream: logger.stream })) + +const router = new express.Router() + +router.route('/ping').get(handler.ping) +router.route('/frameworkRequest').put(handler.receiveFrameworkRequest) +router.route('/watchEvents/:eventType').post(handler.receiveWatchEvents) + +app.use('/api/v1', router) + +// error handling +app.use((err, req, res, next) => { + logger.warn(err.stack) + const statusCode = err.statusCode || 500 + res.status(statusCode).json({ + code: status(statusCode), + message: err.message + }) +}) + +module.exports = app diff --git a/src/database-controller/src/write-merger/config.js b/src/database-controller/src/write-merger/config.js new file mode 100644 index 0000000000..47e40b3e02 --- /dev/null +++ b/src/database-controller/src/write-merger/config.js @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const basicConfig = require('@dbc/core/config') +const _ = require('lodash') +const Joi = require('joi') + +const configSchema = Joi.object().keys({ + dbConnectionStr: Joi.string() + .required(), + maxDatabaseConnection: Joi.number() + .integer() + .required(), + bodyLimit: Joi.string() + .default('100mb'), + port: Joi.number() + .integer() + .required() +}).required() + +const config = { + dbConnectionStr: process.env.DB_CONNECTION_STR, + maxDatabaseConnection: parseInt(process.env.MAX_DB_CONNECTION), + port: parseInt(process.env.PORT) +} + +const { error, value } = Joi.validate(config, configSchema) +if (error) { + throw new Error(`Config error\n${error}`) +} + +module.exports = _.assign(basicConfig, value) diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js new file mode 100644 index 0000000000..cd003b4842 --- /dev/null +++ b/src/database-controller/src/write-merger/handler.js @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +const createError = require('http-errors') +const logger = require('@dbc/core/logger') +const AsyncLock = require('async-lock') +const DatabaseModel = require('openpaidbsdk') +const config = require('@dbc/write-merger/config') +const { Snapshot, AddOns, silentSynchronizeRequest } = require('@dbc/core/framework') +const _ = require('lodash') +const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }) +const databaseModel = new DatabaseModel( + config.dbConnectionStr, + config.maxDatabaseConnection +) + +/* For error handling, all handlers follow the same structure: + + try{ + .... + } catch (err) { + return next(err) + } + + If a normal HTTP error is wanted, use `createError`. + e.g. return next(createError(401, 'Please login to view this page.')) + + If a 500 error is wanted, throw it in-place: throw new Error('fatal error') + +*/ + +async function ping (req, res, next) { + try { + res.status(200).json({ message: 'ok' }) + } catch (err) { + return next(err) + } +} + +async function receiveWatchEvents (req, res, next) { + try { + const snapshot = new Snapshot(req.body) + const frameworkName = snapshot.getName() + if (!frameworkName) { + return next(createError(400, 'Cannot find framework name.')) + } + await lock.acquire(frameworkName, async () => { + const oldFramework = await databaseModel.Framework.findOne({ + attributes: ['snapshot'], + where: { name: frameworkName } + } + ) + // database doesn't have the corresponding framework. + if (!oldFramework) { + if (config.recoveryModeEnabled) { + // If database doesn't have the corresponding framework, + // and recovery mode is enabled + // tolerate the error and create framework in database. + const record = snapshot.getRecordForLegacyTransfer() + record.requestSynced = true + await databaseModel.Framework.create(record) + } else { + throw createError(404, `Cannot find framework ${frameworkName}.`) + } + } else { + // Database has the corresponding framework. + const oldSnapshot = new Snapshot(oldFramework.snapshot) + const internalUpdate = {} + if (oldSnapshot.getGeneration() === snapshot.getGeneration()) { + // if framework request is equal, mark requestSynced = true + logger.info(`The request of framework ${frameworkName} is synced.`) + internalUpdate.requestSynced = true + } else { + // if framework request is not equal, + // should use framework request in db as ground truth + internalUpdate.requestSynced = false + } + // use request in database + snapshot.overrideRequest(oldSnapshot) + if (req.params.eventType === 'DELETED') { + // if event is DELETED, mark apiServerDeleted = true + internalUpdate.apiServerDeleted = true + } + await databaseModel.Framework.update( + _.assign(snapshot.getStatusUpdate(), internalUpdate), + { where: { name: frameworkName } } + ) + } + }) + res.status(200).json({ message: 'ok' }) + } catch (err) { + return next(err) + } +} + +async function receiveFrameworkRequest (req, res, next) { + try { + const { frameworkRequest, submissionTime, configSecretDef, priorityClassDef, dockerSecretDef } = req.body + const frameworkName = _.get(frameworkRequest, 'metadata.name') + if (!frameworkName) { + return next(createError(400, 'Cannot find framework name.')) + } + const [needSynchronize, snapshot, addOns] = await lock.acquire( + frameworkName, async () => { + const oldFramework = await databaseModel.Framework.findOne({ + attributes: ['snapshot'], + where: { name: frameworkName } + } + ) + const snapshot = new Snapshot(frameworkRequest) + const addOns = new AddOns(configSecretDef, priorityClassDef, dockerSecretDef) + if (!oldFramework) { + // create new record in db + // including all add-ons and submissionTime + // set requestGeneration = 1 + snapshot.setGeneration(1) + const record = _.assign({}, snapshot.getAllUpdate(), addOns.getUpdate()) + record.submissionTime = new Date(submissionTime) + await databaseModel.Framework.create(record) + return [true, snapshot, addOns] + } else { + // update record in db + const oldSnapshot = new Snapshot(oldFramework.snapshot) + // compare framework request (omit requestGeneration) + if (_.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true))) { + // request is equal, no-op + return [false, snapshot, addOns] + } else { + // request is different + // update request in db, mark requestSynced=false + snapshot.setGeneration(oldSnapshot.getGeneration() + 1) + await databaseModel.Framework.update( + _.assign({}, snapshot.getRequestUpdate(), { requestSynced: false }), + { where: { name: frameworkName } } + ) + return [true, snapshot, addOns] + } + } + }) + res.status(200).json({ message: 'ok' }) + // skip db poller, any response or error will be ignored + if (needSynchronize) { + silentSynchronizeRequest(snapshot, addOns) + } + } catch (err) { + return next(err) + } +} + +module.exports = { + ping: ping, + receiveFrameworkRequest: receiveFrameworkRequest, + receiveWatchEvents: receiveWatchEvents +} diff --git a/src/database-controller/src/write-merger/index.js b/src/database-controller/src/write-merger/index.js new file mode 100644 index 0000000000..7d9dee2814 --- /dev/null +++ b/src/database-controller/src/write-merger/index.js @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +require('module-alias/register') +require('dotenv').config() +const app = require('@dbc/write-merger/app') +const { port } = require('@dbc/write-merger/config') + +app.listen(port, () => console.log(`Write merger listening on port ${port}!`)) diff --git a/src/database-controller/src/yarn.lock b/src/database-controller/src/yarn.lock new file mode 100644 index 0000000000..2bca50fc56 --- /dev/null +++ b/src/database-controller/src/yarn.lock @@ -0,0 +1,1862 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@kubernetes/client-node@0.11.2": + version "0.11.2" + resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.11.2.tgz#5dbc1d66d3c954af0f3bd294dece884737f5b761" + integrity sha512-Uhwd2y2qCvugICnHRC5h2MT5vw0a1dJPVVltVwmkeMuyGTPBccsTtpTcSfSLitwOrh4yr+9wG5bRcMdgeRjYPw== + dependencies: + "@types/js-yaml" "^3.12.1" + "@types/node" "^10.12.0" + "@types/request" "^2.47.1" + "@types/underscore" "^1.8.9" + "@types/ws" "^6.0.1" + byline "^5.0.0" + execa "1.0.0" + isomorphic-ws "^4.0.1" + js-yaml "^3.13.1" + jsonpath-plus "^0.19.0" + openid-client "2.5.0" + request "^2.88.0" + rfc4648 "^1.3.0" + shelljs "^0.8.2" + tslib "^1.9.3" + underscore "^1.9.1" + ws "^6.1.0" + +"@sindresorhus/is@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" + integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== + +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + +"@types/js-yaml@^3.12.1": + version "3.12.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" + integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== + +"@types/node@*": + version "14.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" + integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== + +"@types/node@^10.12.0": + version "10.17.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.26.tgz#a8a119960bff16b823be4c617da028570779bcfd" + integrity sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw== + +"@types/request@^2.47.1": + version "2.48.5" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" + integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + +"@types/tough-cookie@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" + integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== + +"@types/underscore@^1.8.9": + version "1.10.4" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.4.tgz#0b91aa47205086dfd4c0d85b3434b94e62e6695c" + integrity sha512-2dZ6y+llKA20NZdZgyDE1V9ZaRzcDQf86EUuT/NrTXX3ZtAhPCurIyf6OI/GrE6HuySQoRv0QwzwpUJxHn0WgA== + +"@types/ws@^6.0.1": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" + integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg== + dependencies: + "@types/node" "*" + +accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +aggregate-error@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-1.0.0.tgz#888344dad0220a72e3af50906117f48771925fac" + integrity sha1-iINE2tAiCnLjr1CQYRf0h3GSX6w= + dependencies: + clean-stack "^1.0.0" + indent-string "^3.0.0" + +ajv@^6.5.5: + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async-lock@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.4.tgz#80d0d612383045dd0c30eb5aad08510c1397cb91" + integrity sha512-UBQJC2pbeyGutIfYmErGc9RaJYnpZ1FHaxuKwb0ahvGiiCkPUf3p67Io+YLPmmv3RHY+mF6JEtNW8FlHsraAaA== + +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + integrity sha1-+PwEyjoTeErenhZBr5hXjPvWR6k= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" + integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base64url@^3.0.0, base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.0, body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +buffer@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacheable-request@^2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" + integrity sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0= + dependencies: + clone-response "1.0.2" + get-stream "3.0.0" + http-cache-semantics "3.8.1" + keyv "3.0.0" + lowercase-keys "1.0.0" + normalize-url "2.0.1" + responselike "1.0.2" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +clean-stack@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31" + integrity sha1-noIVAa6XmYbEax1m0tQy2y/UrjE= + +clone-response@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +cls-bluebird@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cls-bluebird/-/cls-bluebird-2.1.0.tgz#37ef1e080a8ffb55c2f4164f536f1919e7968aee" + integrity sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4= + dependencies: + is-bluebird "^1.0.2" + shimmer "^1.1.0" + +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +dotenv@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + +dottie@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.2.tgz#cc91c0726ce3a054ebf11c55fbc92a7f266dd154" + integrity sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg== + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +es6-promise@^4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +execa@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2@^2.1.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stream@3.0.0, get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.0: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +got@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" + integrity sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw== + dependencies: + "@sindresorhus/is" "^0.7.0" + cacheable-request "^2.1.1" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + into-stream "^3.1.0" + is-retry-allowed "^1.1.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + mimic-response "^1.0.0" + p-cancelable "^0.4.0" + p-timeout "^2.0.1" + pify "^3.0.0" + safe-buffer "^5.1.1" + timed-out "^4.0.1" + url-parse-lax "^3.0.0" + url-to-options "^1.0.1" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-symbol-support-x@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" + integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== + +has-to-string-tag-x@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" + integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw== + dependencies: + has-symbol-support-x "^1.4.1" + +hoek@6.x.x: + version "6.1.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" + integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== + +http-cache-semantics@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" + integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + +inflection@1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +interval-promise@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interval-promise/-/interval-promise-1.4.0.tgz#eef1a3633c6c6560488d8ca4b2f52cf901e16f8d" + integrity sha512-PUwEmGqUglJhb6M01JNvMDvxr4DA8FCeYoYCLHPEcBBZiq/8yOpCchfs1VJui7fXj69l170gAxzF1FeSA0nSlg== + +into-stream@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" + integrity sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY= + dependencies: + from2 "^2.1.1" + p-is-promise "^1.1.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-bluebird@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2" + integrity sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI= + +is-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" + integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isemail@3.x.x: + version "3.2.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c" + integrity sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg== + dependencies: + punycode "2.x.x" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + +isstream@0.1.x, isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +isurl@^1.0.0-alpha5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" + integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w== + dependencies: + has-to-string-tag-x "^1.2.0" + is-object "^1.0.1" + +joi@^14.3.1: + version "14.3.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-14.3.1.tgz#164a262ec0b855466e0c35eea2a885ae8b6c703c" + integrity sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ== + dependencies: + hoek "6.x.x" + isemail "3.x.x" + topo "3.x.x" + +js-yaml@^3.13.1, js-yaml@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonpath-plus@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c" + integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg== + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +keyv@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" + integrity sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA== + dependencies: + json-buffer "3.0.0" + +lodash@^4.17.11, lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +lowercase-keys@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= + +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +module-alias@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" + integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== + +moment-timezone@^0.5.21: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.24.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +node-forge@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.5.tgz#57906f07614dc72762c84cef442f427c0e1b86ee" + integrity sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q== + +node-jose@^1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/node-jose/-/node-jose-1.1.4.tgz#af3f44a392e586d26b123b0e12dc09bef1e9863b" + integrity sha512-L31IFwL3pWWcMHxxidCY51ezqrDXMkvlT/5pLTfNw5sXmmOLJuN6ug7txzF/iuZN55cRpyOmoJrotwBQIoo5Lw== + dependencies: + base64url "^3.0.1" + browserify-zlib "^0.2.0" + buffer "^5.5.0" + es6-promise "^4.2.8" + lodash "^4.17.15" + long "^4.0.0" + node-forge "^0.8.5" + process "^0.11.10" + react-zlib-js "^1.0.4" + uuid "^3.3.3" + +normalize-url@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-hash@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" + integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== + +oidc-token-hash@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-3.0.2.tgz#5bd4716cc48ad433f4e4e99276811019b165697e" + integrity sha512-dTzp80/y/da+um+i+sOucNqiPpwRL7M/xPwj7pH1TFA2/bqQ+OK2sJahSXbemEoLtPkHcFLyhLhLWZa9yW5+RA== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +openid-client@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-2.5.0.tgz#7d4cf552b30dbad26917d7e2722422eda057ea93" + integrity sha512-t3hFD7xEoW1U25RyBcRFaL19fGGs6hNVTysq9pgmiltH0IVUPzH/bQV9w24pM5Q7MunnGv2/5XjIru6BQcWdxg== + dependencies: + base64url "^3.0.0" + got "^8.3.2" + lodash "^4.17.11" + lru-cache "^5.1.1" + node-jose "^1.1.0" + object-hash "^1.3.1" + oidc-token-hash "^3.0.1" + p-any "^1.1.0" + +openpaidbsdk@../sdk: + version "1.0.0" + dependencies: + pg "^8.2.1" + sequelize "5.21.3" + +"openpaidbsdk@file:../sdk": + version "1.0.0" + dependencies: + pg "^8.2.1" + sequelize "5.21.3" + +p-any@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-any/-/p-any-1.1.0.tgz#1d03835c7eed1e34b8e539c47b7b60d0d015d4e1" + integrity sha512-Ef0tVa4CZ5pTAmKn+Cg3w8ABBXh+hHO1aV8281dKOoUHfX+3tjG2EaFcC+aZyagg9b4EYGsHEjz21DnEE8Og2g== + dependencies: + p-some "^2.0.0" + +p-cancelable@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" + integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= + +p-some@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-some/-/p-some-2.0.1.tgz#65d87c8b154edbcf5221d167778b6d2e150f6f06" + integrity sha1-Zdh8ixVO289SIdFnd4ttLhUPbwY= + dependencies: + aggregate-error "^1.0.0" + +p-timeout@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" + integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== + dependencies: + p-finally "^1.0.0" + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pg-connection-string@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.2.3.tgz#48e1158ec37eaa82e98dbcb7307103ec303fe0e7" + integrity sha512-I/KCSQGmOrZx6sMHXkOs2MjddrYcqpza3Dtsy0AjIgBr/bZiPJRK9WhABXN1Uy1UDazRbi9gZEzO2sAhL5EqiQ== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0" + integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA== + +pg-protocol@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.2.4.tgz#3139cac0e51347f1e21e03954b1bb9fe2c20962e" + integrity sha512-/8L/G+vW/VhWjTGXpGh8XVkXOFx1ZDY+Yuz//Ab8CfjInzFkreI+fDG3WjCeSra7fIZwAFxzbGptNbm8xSXenw== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.2.1.tgz#f5a81f5e2025182fbe701514d3e1a43e68a616ac" + integrity sha512-DKzffhpkWRr9jx7vKxA+ur79KG+SKw+PdjMb1IRhMiKI9zqYUGczwFprqy+5Veh/DCcFs1Y6V8lRLN5I1DlleQ== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.2.3" + pg-pool "^3.2.1" + pg-protocol "^1.2.4" + pg-types "^2.1.0" + pgpass "1.x" + semver "4.3.2" + +pgpass@1.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" + integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= + dependencies: + split "^1.0.0" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.5.tgz#710b27de5f27d550f6e80b5d34f7ba189213c2ee" + integrity sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-zlib-js@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7" + integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A== + +readable-stream@^2.0.0: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + 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.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +resolve@^1.1.6: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +responselike@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +retry-as-promised@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543" + integrity sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg== + dependencies: + any-promise "^1.3.0" + +rfc4648@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.3.0.tgz#2a69c76f05bc0e388feab933672de9b492af95f1" + integrity sha512-x36K12jOflpm1V8QjPq3I+pt7Z1xzeZIjiC8J2Oxd7bE1efTrOG241DTYVJByP/SxR9jl1t7iZqYxDX864jgBQ== + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" + integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= + +semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +sequelize-pool@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.3.0.tgz#64f1fe8744228172c474f530604b6133be64993d" + integrity sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA== + +sequelize@5.21.3: + version "5.21.3" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.21.3.tgz#f8a6fa0245f8995d70849e4da00c2c7c9aa9f569" + integrity sha512-ptdeAxwTY0zbj7AK8m+SH3z52uHVrt/qmOTSIGo/kyfnSp3h5HeKlywkJf5GEk09kuRrPHfWARVSXH1W3IGU7g== + dependencies: + bluebird "^3.5.0" + cls-bluebird "^2.1.0" + debug "^4.1.1" + dottie "^2.0.0" + inflection "1.12.0" + lodash "^4.17.15" + moment "^2.24.0" + moment-timezone "^0.5.21" + retry-as-promised "^3.2.0" + semver "^6.3.0" + sequelize-pool "^2.3.0" + toposort-class "^1.0.1" + uuid "^3.3.3" + validator "^10.11.0" + wkx "^0.4.8" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shelljs@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shimmer@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + +signal-exit@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.0.tgz#aa7b107e018eb33e08e8aee2e7337e762dda1028" + integrity sha512-w9jNUUQdpuVoYqXxnyOakhckBbOxRaoYqJscyIBYCS5ixyCnO7nQn7zBZvP9zf5QOPZcz2DLUpE3KsNPbJBOFA== + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +through@2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +timed-out@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +topo@3.x.x: + version "3.0.3" + resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c" + integrity sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ== + dependencies: + hoek "6.x.x" + +toposort-class@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" + integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tslib@^1.9.3: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +underscore@^1.9.1: + version "1.10.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf" + integrity sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +url-to-options@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" + integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validator@^10.11.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" + integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +winston@2: + version "2.4.5" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.5.tgz#f2e431d56154c4ea765545fc1003bd340c95b59a" + integrity sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A== + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + +wkx@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.8.tgz#a092cf088d112683fdc7182fd31493b2c5820003" + integrity sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ== + dependencies: + "@types/node" "*" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/src/database-controller/test-case.md b/src/database-controller/test-case.md new file mode 100644 index 0000000000..37f2280c16 --- /dev/null +++ b/src/database-controller/test-case.md @@ -0,0 +1,146 @@ +# Database Controller Test Cases + + - [End-to-end Test](#test-jobs) + - [Path Test](#path-test) + - [Upgrade Test](#upgrade-test) + - [Stress Test](#stress-test) + +## End-to-end Test + +### Test Jobs + +1. A job with simple output + + Case: job command `echo success` + + Expect: Succeed, output `success`. + +2. Stop a job + + Case: job command `sleep 1h`; then stop this job + + Expect: The job can be stopped. + +3. Job with retries + + Case: job command: `exit 1`; set max retry to be 10 + + Expect: The job retry history can be viewed. + +4. Job with docker secret + + Case: Submit a job with docker auth info + + Expect: The job can run successfully. + +5. Job with priority class + + Case: Submit a job with priority class + + Expect: The job priority class is correct. + +6. Job with secret + + Case: Submit a job with secret info + + Expect: The secret info is successfully written into job. + +7. Bulk job + + Case: Submit a job with 1000+ instances. + + Expect: The job can run successfully. + +8. Bulk job with retries + + Case: Submit a job with 1000+ instances, and 50+ retries. + + Expect: The job can run successfully. The job retry history can be viewed. + + +### Test in Certain Case + +1. Database failure + + Case: submit a job; shutdown the database; try to submit another job; start the database + + Expect: the first job is not affected; the second job cannot be submitted. + +2. Database controller failure + + Case: submit a job; shutdown the database controller; try to submit another job; start the database + + Expect: the first job is not affected; the second job cannot be submitted. + +3. Framework controller failure + + Case: shutdown the framework controller; try to submit a job; start the framework controller + + Expect: the job can be submitted, but in `WAITING` status. After the framework controller is started, its state will turn to `RUNNING`. + + +## Path Test + +#### Rest-server checks conflicts + +1. Case: Submit a job twice + + Expect: The second job submission is not allowed. + +2. Case: Stop a non-existed job + + Expect: The operation is not allowed. + +#### (Write merger) Add/Update FR (If not equal, override and mark !requestSynced, else no-op) + +1. Environment: Write-merger doesn't forward request; Shutdown database poller; + + Case: Fake two same framework requests to write-merger. + + Expect: The second request will be ignored. + +#### (Write merger) If recovery mode, use the FR from API server + +1. Environment: Turn on recovery mode + + Case: Create a framework directly in API server; + + Expect: The framework is inserted in database. + +2. Environment: Turn off recovery mode + + Case: Create a framework directly in API server; + + Expect: The framework can't be inserted in database, write merger reports 404 error, and watcher keeps trying to synchroinze it. + +#### (Write merger) If not equal, override and mark as !requestSynced + +1. Environment: Shutdown database poller + + Case: Submit a job; Then change its requestGeneration in API server. + + Expect: The framework is marked as `requestSynced=false`. + +#### (Database poller) If API server 404 error, mock a delete Framework + +1. Case: Submit A job; Then shutdown watcher after the job starts; Wait until the job succeeded; Stop poller; Start watcher; Stop watcher; Delete this framework manually; Start Poller + + Expect: + + After watcher is restarted, the job is marked as `state=Completed` and `requestSynced=true`; + After poller is restarted, it can mock a delete framework event to write merger. + Finally, the job will be marked as `apiServerDeleted=true`. + +## Upgrade Test + +1. Upgrade from `v1.0.y` + + Drop the database; Deploy a `v1.0.y` bed, submit some jobs (must include: one running job, one completed job with retry history, and one completed job without retry history). Then upgrade it. Make sure the job information is correct, and running jobs are not affected. + +2. Upgrade from `v1.1.y` + + Drop the database; Deploy a `v1.1.y` bed, submit some jobs (must include: one running job, one completed job with retry history, and one completed job without retry history). Then upgrade it. Make sure the job information is correct, and running jobs are not affected. + +## Stress Test + +1. Submit 100000+ jobs in 1 hour. It should be handled properly. diff --git a/src/fluentd/config/fluentd.py b/src/fluentd/config/fluentd.py index bed5ba57e5..97c65c4d08 100644 --- a/src/fluentd/config/fluentd.py +++ b/src/fluentd/config/fluentd.py @@ -13,7 +13,4 @@ def run(self): return {} def validation_post(self, conf): - if 'job-history' in conf['cluster']['common'] and conf['cluster']['common']['job-history'] != "false": - if conf['postgresql']['enable'] is False: - return False, "You must set postgresql.enable=true to use job history." return True, None diff --git a/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb b/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb index 4c23e50c0c..a67267460c 100644 --- a/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb +++ b/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb @@ -151,16 +151,17 @@ def write(chunk) kind = record["objectSnapshot"]["kind"] log.debug "log type: #{kind}" if kind == "Framework" - thread[:conn].exec("COPY framework_history (#{@insertedAt_col}, #{@frameworkName_col}, #{@attemptIndex_col}, #{@historyType_col}, #{@snapshot_col}) FROM STDIN WITH DELIMITER E'\\x01'") + thread[:conn].exec("COPY framework_history (\"#{@insertedAt_col}\", \"#{@updatedAt_col}\", \"#{@uid_col}\", \"#{@frameworkName_col}\", \"#{@attemptIndex_col}\", \"#{@historyType_col}\", \"#{@snapshot_col}\") FROM STDIN WITH DELIMITER E'\\x01'") + uid = (0...36).map { (65 + rand(26)).chr }.join frameworkName = record["objectSnapshot"]["metadata"]["name"] attemptIndex = record["objectSnapshot"]["status"]["attemptStatus"]["id"] historyType = "retry" snapshot = record_value(record["objectSnapshot"]) - thread[:conn].put_copy_data "#{time}\x01#{frameworkName}\x01#{attemptIndex}\x01#{historyType}\x01#{snapshot}\n" + thread[:conn].put_copy_data "#{time}\x01#{time}\x01#{uid}\x01#{frameworkName}\x01#{attemptIndex}\x01#{historyType}\x01#{snapshot}\n" elsif kind == "Pod" - thread[:conn].exec("COPY pods (#{@insertedAt_col}, #{@updatedAt_col}, #{@uid_col}, #{@frameworkName_col}, #{@attemptIndex_col}, #{@taskroleName_col}, #{@taskroleIndex_col}, #{@taskAttemptIndex_col}, #{@snapshot_col}) FROM STDIN WITH DELIMITER E'\\x01'") + thread[:conn].exec("COPY pods (\"#{@insertedAt_col}\", \"#{@updatedAt_col}\", \"#{@uid_col}\", \"#{@frameworkName_col}\", \"#{@attemptIndex_col}\", \"#{@taskroleName_col}\", \"#{@taskroleIndex_col}\", \"#{@taskAttemptIndex_col}\", \"#{@snapshot_col}\") FROM STDIN WITH DELIMITER E'\\x01'") uid = record["objectSnapshot"]["metadata"]["uid"] - frameworkName = record["objectSnapshot"]["metadata"]["name"] + frameworkName = record["objectSnapshot"]["metadata"]["name"][0..31] attemptIndex = record["objectSnapshot"]["metadata"]["annotations"]["FC_FRAMEWORK_ATTEMPT_ID"] taskroleName = record["objectSnapshot"]["metadata"]["annotations"]["FC_TASKROLE_NAME"] taskroleIndex = record["objectSnapshot"]["metadata"]["annotations"]["FC_TASK_INDEX"] diff --git a/src/internal-storage/README.md b/src/internal-storage/README.md index 18e679b143..63f47d796c 100644 --- a/src/internal-storage/README.md +++ b/src/internal-storage/README.md @@ -4,7 +4,6 @@ Internal Storage is designed to create a limited size storage in PAI. The storag ```yaml internal-storage: - enable: false type: hostPath root-path: /mnt/paiInternal quota-gb: 10 diff --git a/src/internal-storage/config/internal-storage.yaml b/src/internal-storage/config/internal-storage.yaml index b3fc0ec482..0d6af6c637 100644 --- a/src/internal-storage/config/internal-storage.yaml +++ b/src/internal-storage/config/internal-storage.yaml @@ -17,7 +17,6 @@ service_type: "common" -enable: true type: hostPath root-path: /mnt/paiInternal quota-gb: 10 \ No newline at end of file diff --git a/src/internal-storage/config/internal_storage.py b/src/internal-storage/config/internal_storage.py index c92edc46d9..4934c354dc 100644 --- a/src/internal-storage/config/internal_storage.py +++ b/src/internal-storage/config/internal_storage.py @@ -19,27 +19,23 @@ def merge_service_configuration(overwrite_srv_cfg, default_srv_cfg): return srv_cfg def validation_pre(self): - if self.service_conf['enable']: - type_ = self.service_conf.get('type', '') - if type_ == 'hostPath': - machine_list = self.cluster_conf['machine-list'] - if len([host for host in machine_list if host.get('pai-master') == 'true']) < 1: - return False, '"pai-master=true" machine is required to deploy the internal storage' - quotaGB = int(self.service_conf['quota-gb']) - assert quotaGB >= 1 - return True, None - else: - return False, 'Unknown internal storage type {}'.format(type_) - else: + type_ = self.service_conf.get('type', '') + if type_ == 'hostPath': + machine_list = self.cluster_conf['machine-list'] + if len([host for host in machine_list if host.get('pai-master') == 'true']) < 1: + return False, '"pai-master=true" machine is required to deploy the internal storage' + quotaGB = int(self.service_conf['quota-gb']) + assert quotaGB >= 1 return True, None + else: + return False, 'Unknown internal storage type {}'.format(type_) def run(self): result = copy.deepcopy(self.service_conf) - if result['enable']: - machine_list = self.cluster_conf['machine-list'] - master_ip = [host['hostip'] for host in machine_list if host.get('pai-master') == 'true'][0] - result['master-ip'] = master_ip - result['quota-gb'] = int(result['quota-gb']) + machine_list = self.cluster_conf['machine-list'] + master_ip = [host['hostip'] for host in machine_list if host.get('pai-master') == 'true'][0] + result['master-ip'] = master_ip + result['quota-gb'] = int(result['quota-gb']) return result def validation_post(self, conf): diff --git a/src/internal-storage/deploy/delete.sh.template b/src/internal-storage/deploy/delete.sh.template index e4f44aae0b..92e4719258 100644 --- a/src/internal-storage/deploy/delete.sh.template +++ b/src/internal-storage/deploy/delete.sh.template @@ -21,8 +21,6 @@ pushd $(dirname "$0") > /dev/null kubectl delete --ignore-not-found --now "daemonset/internal-storage-create-ds" -{% if cluster_cfg['internal-storage']['enable'] %} - kubectl apply --overwrite=true -f delete.yaml || exit $? # Wait until the service is ready. @@ -30,6 +28,4 @@ PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ kubectl delete --ignore-not-found --now "daemonset/internal-storage-delete-ds" -{% endif %} - popd > /dev/null diff --git a/src/internal-storage/deploy/start.sh.template b/src/internal-storage/deploy/start.sh.template index 98f355e065..3cba5f1261 100644 --- a/src/internal-storage/deploy/start.sh.template +++ b/src/internal-storage/deploy/start.sh.template @@ -19,13 +19,9 @@ pushd $(dirname "$0") > /dev/null -{% if cluster_cfg['internal-storage']['enable'] %} - kubectl apply --overwrite=true -f create.yaml || exit $? # Wait until the service is ready. PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ready_status -w -k app -v internal-storage-create || exit $? -{% endif %} - popd > /dev/null diff --git a/src/postgresql/README.md b/src/postgresql/README.md index 99405d64c9..6950749996 100644 --- a/src/postgresql/README.md +++ b/src/postgresql/README.md @@ -4,11 +4,11 @@ Postgresql is an internal service for structured information persistence. By def ```yaml postgresql: - enable: false user: root passwd: rootpass port: 5432 db: openpai + max-connection: 1000 ``` One can override these settings by editing `services-configuration.yaml` . diff --git a/src/postgresql/config/postgresql.py b/src/postgresql/config/postgresql.py index 4133e5f2e0..d935b7716f 100644 --- a/src/postgresql/config/postgresql.py +++ b/src/postgresql/config/postgresql.py @@ -37,23 +37,19 @@ def merge_service_configuration(overwrite_srv_cfg, default_srv_cfg): return srv_cfg def validation_pre(self): - if self.service_conf['enable']: - machine_list = self.cluster_conf['machine-list'] - if len([host for host in machine_list if host.get('pai-master') == 'true']) < 1: - return False, '"pai-master=true" machine is required to deploy the postgresql service' + machine_list = self.cluster_conf['machine-list'] + if len([host for host in machine_list if host.get('pai-master') == 'true']) < 1: + return False, '"pai-master=true" machine is required to deploy the postgresql service' return True, None def run(self): result = copy.deepcopy(self.service_conf) - if self.service_conf['enable']: - machine_list = self.cluster_conf['machine-list'] - master_ip = [host['hostip'] for host in machine_list if host.get('pai-master') == 'true'][0] - result['host'] = master_ip - result['connection-str'] = 'postgresql://{}:{}@{}:{}/{}'.format( - result['user'], result['passwd'], result['host'], result['port'], result['db']) + machine_list = self.cluster_conf['machine-list'] + master_ip = [host['hostip'] for host in machine_list if host.get('pai-master') == 'true'][0] + result['host'] = master_ip + result['connection-str'] = 'postgresql://{}:{}@{}:{}/{}'.format( + result['user'], result['passwd'], result['host'], result['port'], result['db']) return result def validation_post(self, conf): - if conf['internal-storage']['enable'] is False and conf['postgresql']['enable'] is True: - return False, "You must set internal-storage.enable=true to use postgresql!" return True, None diff --git a/src/postgresql/config/postgresql.yaml b/src/postgresql/config/postgresql.yaml index 4493265b73..7744045325 100644 --- a/src/postgresql/config/postgresql.yaml +++ b/src/postgresql/config/postgresql.yaml @@ -17,8 +17,8 @@ service_type: "common" -enable: true user: root passwd: rootpass db: openpai -port: 5432 \ No newline at end of file +port: 5432 +max-connection: 1000 \ No newline at end of file diff --git a/src/postgresql/deploy/postgresql.yaml.template b/src/postgresql/deploy/postgresql.yaml.template index ca626e3d12..85c3640188 100644 --- a/src/postgresql/deploy/postgresql.yaml.template +++ b/src/postgresql/deploy/postgresql.yaml.template @@ -43,7 +43,7 @@ spec: value: {{ cluster_cfg["postgresql"]["db"] }} - name: PGDATA value: /var/lib/postgresql/data/pgdata - args: ['-c', 'port={{- cluster_cfg["postgresql"]["port"] }}'] + args: ['-c', 'port={{- cluster_cfg["postgresql"]["port"] }}', '-N', '{{ cluster_cfg["postgresql"]["max-connection"] }}'] volumeMounts: - name: internal-data-dir mountPath: /var/lib/postgresql/data/ diff --git a/src/postgresql/deploy/start.sh.template b/src/postgresql/deploy/start.sh.template index 90b3833616..04f0c52f93 100644 --- a/src/postgresql/deploy/start.sh.template +++ b/src/postgresql/deploy/start.sh.template @@ -19,13 +19,9 @@ pushd $(dirname "$0") > /dev/null -{% if cluster_cfg['postgresql']['enable'] %} - kubectl apply --overwrite=true -f postgresql.yaml || exit $? # Wait until the service is ready. PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ready_status -w -k app -v postgresql || exit $? -{% endif %} - popd > /dev/null diff --git a/src/postgresql/src/init_table.sql b/src/postgresql/src/init_table.sql index e467c5af66..7d98a04e14 100644 --- a/src/postgresql/src/init_table.sql +++ b/src/postgresql/src/init_table.sql @@ -1,21 +1 @@ -/* This init script will be run every time when the database container is started. */ - -CREATE TABLE IF NOT EXISTS framework_history ( - insertedAt Timestamptz, - uid SERIAL, - frameworkName VARCHAR(64), - attemptIndex INTEGER, - historyType VARCHAR(16), - snapshot TEXT -); -CREATE TABLE IF NOT EXISTS pods ( - insertedAt Timestamptz, - updatedAt Timestamptz, - uid VARCHAR(36), - frameworkName VARCHAR(64), - attemptIndex INTEGER, - taskroleName VARCHAR(256), - taskroleIndex INTEGER, - taskAttemptIndex INTEGER, - snapshot TEXT -); +/* This init script will be run every time when the database container is started. */ \ No newline at end of file diff --git a/src/rest-server/.gitignore b/src/rest-server/.gitignore index 792cb92bd9..e5c23c4868 100644 --- a/src/rest-server/.gitignore +++ b/src/rest-server/.gitignore @@ -68,3 +68,5 @@ typings/ # dotenv environment variables file .env + +openpaidbsdk diff --git a/src/rest-server/build/build-pre.sh b/src/rest-server/build/build-pre.sh index bb8caf6340..5502e27d6b 100755 --- a/src/rest-server/build/build-pre.sh +++ b/src/rest-server/build/build-pre.sh @@ -20,5 +20,6 @@ pushd $(dirname "$0") > /dev/null cp -arf "../../../version" "../version" +cp -rf ../../database-controller/sdk ../openpaidbsdk popd > /dev/null diff --git a/src/rest-server/config/rest-server.yaml b/src/rest-server/config/rest-server.yaml index fcca9df538..6cc4c97647 100644 --- a/src/rest-server/config/rest-server.yaml +++ b/src/rest-server/config/rest-server.yaml @@ -31,3 +31,4 @@ github-repository: pai github-path: marketplace debugging-reservation-seconds: 604800 enable-priority-class: "true" +sql-max-connection: 50 diff --git a/src/rest-server/config/rest_server.py b/src/rest-server/config/rest_server.py index 84cfab750c..2d7a1b58b8 100644 --- a/src/rest-server/config/rest_server.py +++ b/src/rest-server/config/rest_server.py @@ -55,7 +55,7 @@ def run(self): 'default-pai-admin-username', 'default-pai-admin-password', 'github-owner', 'github-repository', 'github-path', 'debugging-reservation-seconds', 'enable-priority-class', - 'schedule-port-start', 'schedule-port-end' + 'schedule-port-start', 'schedule-port-end', 'sql-max-connection' ]: service_object_model[k] = self.service_configuration[k] service_object_model['etcd-uris'] = ','.join('http://{0}:4001'.format(host['hostip']) diff --git a/src/rest-server/deploy/rest-server.yaml.template b/src/rest-server/deploy/rest-server.yaml.template index 5b42ae1a77..f3b268612e 100644 --- a/src/rest-server/deploy/rest-server.yaml.template +++ b/src/rest-server/deploy/rest-server.yaml.template @@ -117,10 +117,10 @@ spec: - name: RBAC_IN_CLUSTER value: "false" {% endif %} -{% if cluster_cfg['postgresql']['enable'] %} - name: SQL_CONNECTION_STR value: {{ cluster_cfg['postgresql']['connection-str'] }} -{% endif %} + - name: SQL_MAX_CONNECTION + value: "{{ cluster_cfg['rest-server']['sql-max-connection'] }}" {% if cluster_cfg['cluster']['common']['job-history'] != 'false' %} - name: JOB_HISTORY value: "true" @@ -128,6 +128,8 @@ spec: - name: JOB_HISTORY value: "false" {% endif %} + - name: WRITE_MERGER_URL + value: {{ cluster_cfg['database-controller']['write-merger-url'] }} ports: - name: rest-server containerPort: 8080 diff --git a/src/rest-server/deploy/service.yaml b/src/rest-server/deploy/service.yaml index 7f22e4eaf9..a1df84630c 100644 --- a/src/rest-server/deploy/service.yaml +++ b/src/rest-server/deploy/service.yaml @@ -26,6 +26,7 @@ prerequisite: - hivedscheduler - log-manager - postgresql + - database-controller template-list: - rest-server.yaml diff --git a/src/rest-server/docs/swagger.yaml b/src/rest-server/docs/swagger.yaml index 51a7776acd..c291aff64f 100644 --- a/src/rest-server/docs/swagger.yaml +++ b/src/rest-server/docs/swagger.yaml @@ -1097,18 +1097,55 @@ paths: security: - bearerAuth: [] parameters: - - name: username + - name: userName in: query description: filter jobs with username schema: type: string + - name: vc + in: query + description: filter jobs with virtual cluster name + schema: + type: string + - name: state + in: query + description: filter jobs with state + schema: + type: string + - name: keyword + in: query + description: filter jobs with keyword, we search keyword in user name, job name, and virtual cluster name + schema: + type: string + - name: offset + in: query + description: list job offset + schema: + type: number + - name: limit + in: query + description: list job limit + schema: + type: number + - name: order + in: query + description: 'order of job list. It follows the format ,, default value is "submissionTime,DESC". Available fields include: jobName, submissionTime, userName, vc, retries, totalTaskNumber, totalGpuNumber, state' + schema: + type: string + - name: withTotalCount + in: query + description: 'if withTotalCount is "true", the result will contain a "totalCount" and a "data" field, instead of a job list.' + schema: + type: string responses: "200": description: Succeeded content: application/json: schema: - $ref: "#/components/schemas/JobSummary" + oneOf: + - $ref: "#/components/schemas/JobSummary" + - $ref: "#/components/schemas/JobSummaryWithTotalCount" example: - protocolVersion: "2" name: job name @@ -1117,6 +1154,7 @@ paths: subState: Completed executionType: STOP retries: 0 + submissionTime: 0 createdTime: 0 completedTime: 0 appExitCode: 0 @@ -1150,6 +1188,7 @@ paths: subState: Completed executionType: STOP retries: 0 + submissionTime: 0 createdTime: 0 completedTime: 0 appId: id @@ -1760,6 +1799,17 @@ components: - name - type - taskRoles + JobSummaryWithTotalCount: + type: object + properties: + totalCount: + type: number + description: total count of jobs with given filters + data: + $ref: "#/components/schemas/JobSummary" + required: + - totalCount + - data JobSummary: type: array description: job summary list @@ -1818,6 +1868,9 @@ components: type: integer nullable: true description: retry delay time + submissionTime: + type: integer + description: job submitted time, in number of milliseconds since the Unix Epoch. createdTime: type: integer description: job created time, in number of milliseconds since the Unix Epoch. @@ -1915,6 +1968,11 @@ components: type: integer nullable: true description: retry delay time + submissionTime: + type: integer + description: >- + job submitted time, in number of milliseconds since the Unix + Epoch. createdTime: type: integer description: >- diff --git a/src/rest-server/package.json b/src/rest-server/package.json index 6ca08b7f98..dcb04d53a1 100644 --- a/src/rest-server/package.json +++ b/src/rest-server/package.json @@ -54,6 +54,7 @@ "nock": "~9.1.6", "node-cache": "~4.2.0", "nyc": "^14.1.1", + "openpaidbsdk": "file:./openpaidbsdk", "pg": "^7.17.1", "querystring": "~0.2.0", "sequelize": "^5.21.3", @@ -70,6 +71,7 @@ "scripts": { "coveralls": "nyc report --reporter=text-lcov | coveralls ..", "lint": "eslint .", + "lintfix": "eslint . --fix", "mocha": "mocha --file ./test/setup --ui bdd --recursive --timeout 1000 --exit", "start": "node index.js", "test": "npm run lint && nyc npm run mocha" diff --git a/src/rest-server/src/config/launcher.js b/src/rest-server/src/config/launcher.js index 5eae624275..4e7765ac00 100644 --- a/src/rest-server/src/config/launcher.js +++ b/src/rest-server/src/config/launcher.js @@ -45,8 +45,13 @@ const k8sLauncherConfigSchema = Joi.object().keys({ requestHeaders: Joi.object(), sqlConnectionString: Joi.string() .required(), + sqlMaxConnection: Joi.number() + .integer() + .required(), enabledJobHistory: Joi.boolean() .required(), + writeMergerUrl: Joi.string() + .required(), healthCheckPath: Joi.func() .arity(0) .required(), @@ -91,7 +96,9 @@ if (launcherType === 'k8s') { 'Content-Type': 'application/json', }, sqlConnectionString: process.env.SQL_CONNECTION_STR || 'unset', + sqlMaxConnection: parseInt(process.env.SQL_MAX_CONNECTION), enabledJobHistory: process.env.JOB_HISTORY === 'true', + writeMergerUrl: process.env.WRITE_MERGER_URL, healthCheckPath: () => { return `/apis/${launcherConfig.apiVersion}`; }, diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 6eaaa6389f..ad60273bab 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -22,16 +22,63 @@ const status = require('statuses'); const asyncHandler = require('@pai/middlewares/v2/asyncHandler'); const createError = require('@pai/utils/error'); const job = require('@pai/models/v2/job'); +const {Op} = require('sequelize'); const list = asyncHandler(async (req, res) => { + // ?keyword=&userName=,&vc=, + // &state=,&offset=&limit=&withTotalCount=true + // &order=state,DESC const filters = {}; + let offset = 0; + let limit; + let withTotalCount = false; + let order = []; if (req.query) { - if ('username' in req.query) { - filters['labelSelector'] = `userName=${req.query.username}`; + if ('userName' in req.query) { + filters.userName = req.query.userName.split(','); + } + if ('vc' in req.query) { + filters.virtualCluster = req.query.vc.split(','); + } + if ('state' in req.query) { + filters.state = req.query.state.split(','); + } + if ('offset' in req.query) { + offset = parseInt(req.query.offset); + } + if ('limit' in req.query) { + limit = parseInt(req.query.limit); + } + if ('withTotalCount' in req.query && req.query.withTotalCount === 'true') { + withTotalCount = true; + } + if ('keyword' in req.query) { + // match text in username, jobname, or vc + filters[Op.or] = [ + {'userName': {[Op.substring]: req.query.keyword}}, + {'jobName': {[Op.substring]: req.query.keyword}}, + {'virtualCluster': {[Op.substring]: req.query.keyword}}, + ]; + } + if ('order' in req.query) { + const {field, ordering} = req.query.order.split(','); + if (['jobName', 'submissionTime', 'userName', 'vc', 'retries', 'totalTaskNumber', + 'totalGpuNumber', 'state'].includes(field)) { + if (ordering === 'ASC' || ordering === 'DESC') { + order.push([field, ordering]); + } + } + } + if (order.length === 0) { + // default order is submissionTime,DESC + order.push(['submissionTime', 'DESC']); } } - const data = await job.list(filters); + const attributes = ['name', 'jobName', 'userName', 'executionType', 'submissionTime', 'creationTime', 'virtualCluster', + 'totalGpuNumber', 'totalTaskNumber', 'totalTaskRoleNumber', 'retries', 'retryDelayTime', 'platformRetries', + 'resourceRetries', 'userRetries', 'completionTime', 'appExitCode', 'subState', 'state']; + const data = await job.list(attributes, filters, order, offset, limit, withTotalCount); res.json(data); }); diff --git a/src/rest-server/src/models/v2/job-attempt.js b/src/rest-server/src/models/v2/job-attempt.js index b621fd2ef3..f11ee5ea97 100644 --- a/src/rest-server/src/models/v2/job-attempt.js +++ b/src/rest-server/src/models/v2/job-attempt.js @@ -17,14 +17,11 @@ // module dependencies const crypto = require('crypto'); -const {isNil} = require('lodash'); const {convertToJobAttempt} = require('@pai/utils/frameworkConverter'); const launcherConfig = require('@pai/config/launcher'); -const createError = require('@pai/utils/error'); -const k8sModel = require('@pai/models/kubernetes/kubernetes'); const logger = require('@pai/config/logger'); -const {sequelize} = require('@pai/utils/postgresUtil'); +const databaseModel = require('@pai/utils/dbUtils'); const convertName = (name) => { // convert framework name to fit framework controller spec @@ -41,10 +38,10 @@ const encodeName = (name) => { } }; -if (sequelize && launcherConfig.enabledJobHistory) { +if (launcherConfig.enabledJobHistory) { const healthCheck = async () => { try { - await sequelize.authenticate(); + await databaseModel.ping(); return true; } catch (e) { logger.error(e.message); @@ -54,52 +51,40 @@ if (sequelize && launcherConfig.enabledJobHistory) { const list = async (frameworkName) => { let attemptData = []; - let uid; + const encodedFrameworkName = encodeName(frameworkName); // get latest framework from k8s API - let response; + let framework; try { - response = await k8sModel.getClient().get( - launcherConfig.frameworkPath(encodeName(frameworkName)), - { - headers: launcherConfig.requestHeaders, - } + framework = await databaseModel.Framework.findOne({ + attributes: ['snapshot'], + where: {name: encodedFrameworkName}} ); } catch (error) { - logger.error(`error when getting framework from k8s api: ${error.message}`); - if (error.response != null) { - response = error.response; - } else { - throw error; - } + logger.error(`error when getting framework from database: ${error.message}`); + throw error; } - if (response.status === 200) { - // get UID from k8s framework API - uid = response.data.metadata.uid; + if (framework) { attemptData.push({ - ...(await convertToJobAttempt(response.data)), + ...(await convertToJobAttempt(JSON.parse(framework.snapshot))), isLatest: true, }); - } else if (response.status === 404) { - logger.warn(`could not get framework ${uid} from k8s: ${JSON.stringify(response)}`); - return {status: 404, data: null}; } else { - throw createError(response.status, 'UnknownError', response.data.message); - } - - if (isNil(uid)) { + logger.warn(`could not get framework ${encodedFrameworkName} from database.`); return {status: 404, data: null}; } - const sqlSentence = `SELECT snapshot as data FROM framework_history WHERE ` + - `frameworkName = '${encodeName(frameworkName)}' ` + - `ORDER BY uid ASC;`; - const pgResult = (await sequelize.query(sqlSentence))[0]; + const historyFrameworks = await databaseModel.FrameworkHistory.findAll({ + attributes: ['snapshot'], + where: {frameworkName: encodedFrameworkName}, + order: [['attemptIndex', 'ASC']], + } + ); const jobRetries = await Promise.all( - pgResult.map((row) => { - return convertToJobAttempt(JSON.parse(row.data)); + historyFrameworks.map((row) => { + return convertToJobAttempt(JSON.parse(row.snapshot)); }), ); attemptData.push( @@ -111,58 +96,43 @@ if (sequelize && launcherConfig.enabledJobHistory) { }; const get = async (frameworkName, jobAttemptIndex) => { - let uid; let attemptFramework; - let response; + let framework; + const encodedFrameworkName = encodeName(frameworkName); try { - response = await k8sModel.getClient().get( - launcherConfig.frameworkPath(encodeName(frameworkName)), - { - headers: launcherConfig.requestHeaders, - } + framework = await databaseModel.Framework.findOne({ + attributes: ['snapshot'], + where: {name: encodedFrameworkName}} ); } catch (error) { - logger.error(`error when getting framework from k8s api: ${error.message}`); - if (error.response != null) { - response = error.response; - } else { - throw error; - } + logger.error(`error when getting framework from database: ${error.message}`); + throw error; } - if (response.status === 200) { - // get uid from k8s framwork API - uid = response.data.metadata.uid; - attemptFramework = response.data; - } else if (response.status === 404) { - logger.warn(`could not get framework ${uid} from k8s: ${JSON.stringify(response)}`); - return {status: 404, data: null}; + if (framework) { + attemptFramework = JSON.parse(framework.snapshot); } else { - throw createError(response.status, 'UnknownError', response.data.message); + logger.warn(`could not get framework ${encodedFrameworkName} from database.`); + return {status: 404, data: null}; } if (jobAttemptIndex < attemptFramework.spec.retryPolicy.maxRetryCount) { - if (isNil(uid)) { - return {status: 404, data: null}; - } - const sqlSentence = `SELECT snapshot as data FROM framework_history WHERE ` + - `frameworkName = '${encodeName(frameworkName)}' and ` + - `attemptIndex = '${jobAttemptIndex}' ` + - `ORDER BY uid ASC;`; - const pgResult = (await sequelize.query(sqlSentence))[0]; + const historyFramework = await databaseModel.FrameworkHistory.findOne({ + attributes: ['snapshot'], + where: {frameworkName: encodedFrameworkName, attemptIndex: jobAttemptIndex}, + }); - if (pgResult.length === 0) { + if (!historyFramework) { return {status: 404, data: null}; } else { - attemptFramework = JSON.parse(pgResult[0].data); + attemptFramework = JSON.parse(historyFramework.snapshot); const attemptDetail = await convertToJobAttempt(attemptFramework); return {status: 200, data: {...attemptDetail, isLatest: false}}; } } else if ( jobAttemptIndex === attemptFramework.spec.retryPolicy.maxRetryCount ) { - // get latest frameworks from k8s API const attemptDetail = await convertToJobAttempt(attemptFramework); return {status: 200, data: {...attemptDetail, isLatest: true}}; } else { diff --git a/src/rest-server/src/models/v2/job/k8s.js b/src/rest-server/src/models/v2/job/k8s.js index 04a9dc7a51..360cec4c17 100644 --- a/src/rest-server/src/models/v2/job/k8s.js +++ b/src/rest-server/src/models/v2/job/k8s.js @@ -22,15 +22,12 @@ const zlib = require('zlib'); const yaml = require('js-yaml'); const crypto = require('crypto'); const status = require('statuses'); -const querystring = require('querystring'); const runtimeEnv = require('./runtime-env'); const launcherConfig = require('@pai/config/launcher'); const createError = require('@pai/utils/error'); const protocolSecret = require('@pai/utils/protocolSecret'); const userModel = require('@pai/models/v2/user'); const storageModel = require('@pai/models/v2/storage'); -const k8sModel = require('@pai/models/kubernetes/kubernetes'); -const k8sSecret = require('@pai/models/kubernetes/k8s-secret'); const env = require('@pai/utils/env'); const path = require('path'); const fs = require('fs'); @@ -38,6 +35,7 @@ const _ = require('lodash'); const logger = require('@pai/config/logger'); const {apiserver} = require('@pai/config/kubernetes'); const schedulePort = require('@pai/config/schedule-port'); +const databaseModel = require('@pai/utils/dbUtils'); let exitSpecPath; if (process.env[env.exitSpecPath]) { @@ -129,52 +127,29 @@ const convertState = (state, exitCode, retryDelaySec) => { } }; -const mockFrameworkStatus = () => { - return { - state: 'AttemptCreationPending', - attemptStatus: { - completionStatus: null, - taskRoleStatuses: [], - }, - retryPolicyStatus: { - retryDelaySec: null, - totalRetriedCount: 0, - accountableRetriedCount: 0, - }, - }; -}; - const convertFrameworkSummary = (framework) => { - if (!framework.status) { - framework.status = mockFrameworkStatus(); - } - const completionStatus = framework.status.attemptStatus.completionStatus; return { - debugId: framework.metadata.name, - name: decodeName(framework.metadata.name, framework.metadata.annotations), - username: framework.metadata.labels ? framework.metadata.labels.userName : 'unknown', - state: convertState( - framework.status.state, - completionStatus ? completionStatus.code : null, - framework.status.retryPolicyStatus.retryDelaySec, - ), - subState: framework.status.state, - executionType: framework.spec.executionType.toUpperCase(), - retries: framework.status.retryPolicyStatus.totalRetriedCount, + debugId: framework.name, + name: framework.jobName, + username: framework.userName, + state: framework.state, + subState: framework.subState, + executionType: framework.executionType.toUpperCase(), + retries: framework.retries, retryDetails: { - user: framework.status.retryPolicyStatus.accountableRetriedCount, - platform: framework.status.retryPolicyStatus.totalRetriedCount - framework.status.retryPolicyStatus.accountableRetriedCount, - resource: 0, + user: framework.userRetries, + platform: framework.platformRetries, + resource: framework.resourceRetries, }, - retryDelayTime: framework.status.retryPolicyStatus.retryDelaySec, - createdTime: new Date(framework.metadata.creationTimestamp).getTime(), - completedTime: new Date(framework.status.completionTime).getTime() || null, - appExitCode: completionStatus ? completionStatus.code : null, - virtualCluster: framework.metadata.labels ? framework.metadata.labels.virtualCluster : 'unknown', - totalGpuNumber: framework.metadata.annotations ? parseInt(framework.metadata.annotations.totalGpuNumber) : 0, - totalTaskNumber: framework.spec.taskRoles.reduce( - (num, spec) => num + spec.taskNumber, 0), - totalTaskRoleNumber: framework.spec.taskRoles.length, + retryDelayTime: framework.retryDelayTime, + submissionTime: new Date(framework.submissionTime).getTime(), + createdTime: new Date(framework.creationTime).getTime() || null, + completedTime: new Date(framework.completionTime).getTime() || null, + appExitCode: framework.appExitCode, + virtualCluster: framework.virtualCluster ? framework.virtualCluster : 'unknown', + totalGpuNumber: framework.totalGpuNumber, + totalTaskNumber: framework.totalTaskNumber, + totalTaskRoleNumber: framework.totalTaskRoleNumber, }; }; @@ -248,9 +223,6 @@ const convertTaskDetail = async (taskStatus, ports, logPathPrefix) => { }; const convertFrameworkDetail = async (framework) => { - if (!framework.status) { - framework.status = mockFrameworkStatus(); - } const attemptStatus = framework.status.attemptStatus; // check fields which may be compressed if (attemptStatus.taskRoleStatuses == null) { @@ -724,7 +696,7 @@ const generateFrameworkDescription = (frameworkName, virtualCluster, config, raw return frameworkDescription; }; -const createPriorityClass = async (frameworkName, priority) => { +const getPriorityClassDef = (frameworkName, priority) => { const priorityClass = { apiVersion: 'scheduling.k8s.io/v1', kind: 'PriorityClass', @@ -736,40 +708,10 @@ const createPriorityClass = async (frameworkName, priority) => { globalDefault: false, }; - let response; - try { - response = await k8sModel.getClient().request({ - method: 'post', - url: launcherConfig.priorityClassesPath(), - headers: launcherConfig.requestHeaders, - data: priorityClass, - }); - } catch (error) { - if (error.response != null) { - response = error.response; - } else { - throw error; - } - } - if (response.status !== status('Created')) { - throw createError(response.status, 'UnknownError', response.data.message); - } + return priorityClass; }; -const deletePriorityClass = async (frameworkName) => { - try { - await k8sModel.getClient().delete( - launcherConfig.priorityClassPath(`${encodeName(frameworkName)}-priority`), - { - headers: launcherConfig.requestHeaders, - } - ); - } catch (error) { - logger.warn('Failed to delete priority class', error); - } -}; - -const createDockerSecret = async (frameworkName, auths) => { +const getDockerSecretDef = (frameworkName, auths) => { const cred = { auths: {}, }; @@ -783,130 +725,81 @@ const createDockerSecret = async (frameworkName, auths) => { auth: Buffer.from(`${username}:${password}`).toString('base64'), }; } - await k8sSecret.create( - 'default', - `${encodeName(frameworkName)}-regcred`, - {'.dockerconfigjson': JSON.stringify(cred)}, - {type: 'kubernetes.io/dockerconfigjson'}, - ); -}; - -const patchDockerSecretOwner = async (frameworkName, frameworkUid) => { - const metadata = { - ownerReferences: [{ - apiVersion: launcherConfig.apiVersion, - kind: 'Framework', - name: encodeName(frameworkName), - uid: frameworkUid, - controller: true, - blockOwnerDeletion: true, - }], + return { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: `${encodeName(frameworkName)}-regcred`, + namespace: 'default', + }, + data: {'.dockerconfigjson': Buffer.from(JSON.stringify(cred)).toString('base64')}, + type: 'kubernetes.io/dockerconfigjson', }; - try { - await k8sSecret.patchMetadata('default', `${encodeName(frameworkName)}-regcred`, metadata); - } catch (error) { - logger.warn('Failed to patch owner reference for secret', error); - } }; -const deleteDockerSecret = async (frameworkName) => { - try { - await k8sSecret.remove('default', `${encodeName(frameworkName)}-regcred`); - } catch (error) { - logger.warn('Failed to delete docker secret', error); - } -}; -const createJobConfigSecret = async (frameworkName, secrets) => { +const getConfigSecretDef = (frameworkName, secrets) => { const data = { - 'secrets.yaml': yaml.safeDump(secrets), + 'secrets.yaml': Buffer.from(yaml.safeDump(secrets)).toString('base64'), }; - await k8sSecret.create( - 'default', - `${encodeName(frameworkName)}-configcred`, - data, - ); -}; - -const patchJobConfigSecretOwner = async (frameworkName, frameworkUid) => { - const metadata = { - ownerReferences: [{ - apiVersion: launcherConfig.apiVersion, - kind: 'Framework', - name: encodeName(frameworkName), - uid: frameworkUid, - controller: true, - blockOwnerDeletion: true, - }], + return { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: `${encodeName(frameworkName)}-configcred`, + namespace: 'default', + }, + data: data, + type: 'Opaque', }; - try { - await k8sSecret.patchMetadata('default', `${encodeName(frameworkName)}-configcred`, metadata); - } catch (error) { - logger.warn('Failed to patch owner reference for secret', error); - } -}; - -const deleteJobConfigSecret = async (frameworkName) => { - try { - await k8sSecret.remove('default', `${encodeName(frameworkName)}-configcred`); - } catch (error) { - logger.warn('Failed to delete protocol secret', error); - } }; -const list = async (filters) => { - // send request to framework controller - let response; +const list = async (attributes, filters, order, offset, limit, withTotalCount) => { + let frameworks; + let totalCount; try { - response = await k8sModel.getClient().get( - `${launcherConfig.frameworksPath()}?${querystring.stringify(filters)}`, - { - headers: launcherConfig.requestHeaders, - } - ); + frameworks = await databaseModel.Framework.findAll({ + attributes: attributes, + where: filters, + offset: offset, + limit: limit, + order: order, + }); + if (withTotalCount) { + totalCount = await databaseModel.Framework.count({where: filters}); + } } catch (error) { - if (error.response != null) { - response = error.response; - } else { throw error; - } } - - if (response.status === status('OK')) { - return response.data.items - .filter((item) => checkName(item.metadata.name)) - .map(convertFrameworkSummary) - .sort((a, b) => b.createdTime - a.createdTime); + frameworks = frameworks + .filter((item) => checkName(item.name)) + .map(convertFrameworkSummary); + if (withTotalCount) { + return { + totalCount: totalCount, + data: frameworks, + }; } else { - throw createError(response.status, 'UnknownError', response.data.message); + return frameworks; } }; const get = async (frameworkName) => { - // send request to framework controller - let response; + let framework; try { - response = await k8sModel.getClient().get( - launcherConfig.frameworkPath(encodeName(frameworkName)), - { - headers: launcherConfig.requestHeaders, - } - ); + framework = await databaseModel.Framework.findOne({ + attributes: ['submissionTime', 'snapshot'], + where: {name: encodeName(frameworkName)}, + }); } catch (error) { - if (error.response != null) { - response = error.response; - } else { - throw error; - } - } - - if (response.status === status('OK')) { - return (await convertFrameworkDetail(response.data)); + throw error; } - if (response.status === status('Not Found')) { - throw createError('Not Found', 'NoJobError', `Job ${frameworkName} is not found.`); + if (framework) { + const frameworkDetail = await convertFrameworkDetail(JSON.parse(framework.snapshot)); + frameworkDetail.jobStatus.submissionTime = new Date(framework.submissionTime).getTime(); + return frameworkDetail; } else { - throw createError(response.status, 'UnknownError', response.data.message); + throw createError('Not Found', 'NoJobError', `Job ${frameworkName} is not found.`); } }; @@ -974,76 +867,92 @@ const put = async (frameworkName, config, rawConfig) => { } const frameworkDescription = generateFrameworkDescription(frameworkName, virtualCluster, config, rawConfig); - // generate image pull secret const auths = Object.values(config.prerequisites.dockerimage) .filter((dockerimage) => dockerimage.auth != null) .map((dockerimage) => dockerimage.auth); - auths.length && await createDockerSecret(frameworkName, auths); + const dockerSecretDef = auths.length ? getDockerSecretDef(frameworkName, auths) : null; // generate job config secret - config.secrets && await createJobConfigSecret(frameworkName, config.secrets); + const configSecretDef = config.secrets ? getConfigSecretDef(frameworkName, config.secrets) : null; // calculate pod priority // reference: https://github.com/microsoft/pai/issues/3704 + const submissionTime = new Date(); + let priorityClassDef = null; if (launcherConfig.enabledPriorityClass) { let jobPriority = 0; if (launcherConfig.enabledHived) { jobPriority = parseInt(Object.values(config.taskRoles)[0].hivedPodSpec.priority); jobPriority = Math.min(Math.max(jobPriority, -1), 126); } - const jobCreationTime = Math.floor(new Date() / 1000) & (Math.pow(2, 23) - 1); + const jobCreationTime = Math.floor(submissionTime / 1000) & (Math.pow(2, 23) - 1); const podPriority = - (((126 - jobPriority) << 23) + jobCreationTime); // create priority class - await createPriorityClass(frameworkName, podPriority); + priorityClassDef = getPriorityClassDef(frameworkName, podPriority); } // send request to framework controller let response; try { - response = await k8sModel.getClient().request({ - method: 'post', - url: launcherConfig.frameworksPath(), - headers: launcherConfig.requestHeaders, - data: frameworkDescription, + response = await axios({ + method: 'put', + url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest', + data: { + frameworkRequest: frameworkDescription, + submissionTime: submissionTime, + configSecretDef: configSecretDef, + priorityClassDef: priorityClassDef, + dockerSecretDef: dockerSecretDef, + }, + headers: { + 'Content-Type': 'application/json', + }, }); } catch (error) { if (error.response != null) { response = error.response; } else { - // do not await for delete - auths.length && deleteDockerSecret(frameworkName); - config.secrets && deleteJobConfigSecret(frameworkName); - launcherConfig.enabledPriorityClass && deletePriorityClass(frameworkName); throw error; } } - if (response.status !== status('Created')) { - // do not await for delete - auths.length && deleteDockerSecret(frameworkName); - config.secrets && deleteJobConfigSecret(frameworkName); - launcherConfig.enabledPriorityClass && deletePriorityClass(frameworkName); + if (response.status !== status('OK')) { throw createError(response.status, 'UnknownError', response.data.message); } - // do not await for patch - auths.length && patchDockerSecretOwner(frameworkName, response.data.metadata.uid); - config.secrets && patchJobConfigSecretOwner(frameworkName, response.data.metadata.uid); }; const execute = async (frameworkName, executionType) => { - // send request to framework controller let response; try { - const headers = {...launcherConfig.requestHeaders}; - headers['Content-Type'] = 'application/merge-patch+json'; - response = await k8sModel.getClient().request({ - method: 'patch', - url: launcherConfig.frameworkPath(encodeName(frameworkName)), - headers, + const framework = await databaseModel.Framework.findOne({ + attributes: ['snapshot', 'submissionTime', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef'], + where: {name: encodeName(frameworkName)}, + }); + if (!framework) { + throw createError('Not Found', 'NoJobError', `Job ${frameworkName} is not found.`); + } + const snapshot = JSON.parse(framework.snapshot); + const frameworkRequest = _.pick(snapshot, [ + 'apiVersion', + 'kind', + 'metadata.name', + 'metadata.labels', + 'metadata.annotations', + 'spec', + ]); + frameworkRequest.spec.executionType = `${executionType.charAt(0)}${executionType.slice(1).toLowerCase()}`; + response = await axios({ + method: 'put', + url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest', data: { - spec: { - executionType: `${executionType.charAt(0)}${executionType.slice(1).toLowerCase()}`, - }, + frameworkRequest: frameworkRequest, + submissionTime: framework.submissionTime, + configSecretDef: framework.configSecretDef, + priorityClassDef: framework.priorityClassDef, + dockerSecretDef: framework.dockerSecretDef, + }, + headers: { + 'Content-Type': 'application/json', }, }); } catch (error) { @@ -1059,34 +968,24 @@ const execute = async (frameworkName, executionType) => { }; const getConfig = async (frameworkName) => { - // send request to framework controller - let response; + let framework; try { - response = await k8sModel.getClient().get( - launcherConfig.frameworkPath(encodeName(frameworkName)), - { - headers: launcherConfig.requestHeaders, - } - ); + framework = await databaseModel.Framework.findOne({ + attributes: ['jobConfig'], + where: {name: encodeName(frameworkName)}, + }); } catch (error) { - if (error.response != null) { - response = error.response; - } else { throw error; - } } - if (response.status === status('OK')) { - if (response.data.metadata.annotations && response.data.metadata.annotations.config) { - return yaml.safeLoad(response.data.metadata.annotations.config); + if (framework) { + if (framework.jobConfig) { + return yaml.safeLoad(framework.jobConfig); } else { throw createError('Not Found', 'NoJobConfigError', `Config of job ${frameworkName} is not found.`); } - } - if (response.status === status('Not Found')) { - throw createError('Not Found', 'NoJobError', `Job ${frameworkName} is not found.`); } else { - throw createError(response.status, 'UnknownError', response.data.message); + throw createError('Not Found', 'NoJobError', `Job ${frameworkName} is not found.`); } }; diff --git a/src/rest-server/src/utils/postgresUtil.js b/src/rest-server/src/utils/dbUtils.js similarity index 77% rename from src/rest-server/src/utils/postgresUtil.js rename to src/rest-server/src/utils/dbUtils.js index 43b6b5f2d3..82fd4060d3 100644 --- a/src/rest-server/src/utils/postgresUtil.js +++ b/src/rest-server/src/utils/dbUtils.js @@ -15,26 +15,12 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -const {Sequelize} = require('sequelize'); +const DatabaseModel = require('openpaidbsdk'); const launcherConfig = require('@pai/config/launcher'); -if (launcherConfig.sqlConnectionString !== 'unset') { - const sequelize = new Sequelize( - launcherConfig.sqlConnectionString, - { - pool: { - max: 10, - min: 1, - }, - } - ); +const databaseModel = new DatabaseModel( + launcherConfig.sqlConnectionString, + launcherConfig.sqlMaxConnection, +); - module.exports = { - sequelize: sequelize, - }; -} else { - module.exports = { - sequelize: null, - }; -} +module.exports = databaseModel; diff --git a/src/rest-server/test/setup.js b/src/rest-server/test/setup.js index b82011e6fa..8664ec4315 100644 --- a/src/rest-server/test/setup.js +++ b/src/rest-server/test/setup.js @@ -38,6 +38,9 @@ process.env.HIVED_SPEC_PATH = 'test/data/hivedscheduler.yaml'; process.env.GROUP_CONFIG_PATH = 'test/data/group.yaml'; process.env[env.exitSpecPath] = 'test/data/exit-spec.yaml'; process.env.REST_SERVER_URI = 'http://restserver.test.pai.9186'; +process.env.SQL_CONNECTION_STR = 'postgres://localhost:5432/openpai'; +process.env.SQL_MAX_CONNECTION = 10; +process.env.WRITE_MERGER_URL = 'http://localhost'; const jwt = require('jsonwebtoken'); const mustache = require('mustache'); diff --git a/src/rest-server/text b/src/rest-server/text new file mode 100644 index 0000000000..ba2a34d806 --- /dev/null +++ b/src/rest-server/text @@ -0,0 +1,17 @@ + +> pai-rest-server@0.0.0 start C:\Users\zhiyuhe\project\pai\src\rest-server +> node index.js + +2020-06-30T04:07:47.240Z [INFO] Create admin group configured in configuration. +2020-06-30T04:07:47.968Z [INFO] config: {"env":"production","logLevel":"debug","serverPort":8080,"jwtSecret":"pai-secret"} +2020-06-30T04:07:47.985Z [INFO] RESTful API server starts on port 8080 +2020-06-30T04:07:49.013Z [INFO] connected to framework controller successfully +2020-06-30T04:07:49.016Z [INFO] Namespace pai-user-token already exists +2020-06-30T04:07:49.045Z [INFO] Create admin group successfully. +2020-06-30T04:07:49.045Z [INFO] create default vc's group. +2020-06-30T04:07:50.087Z [INFO] Create default group successfully. +2020-06-30T04:07:50.087Z [INFO] Create group configured in configuration. +2020-06-30T04:07:50.088Z [INFO] Create group successfully. +2020-06-30T04:07:51.634Z [INFO] Create admin user account configured in configuration. +2020-06-30T04:07:52.655Z [INFO] Create admin user account successfully. +Terminate batch job (Y/N)? diff --git a/src/rest-server/yarn.lock b/src/rest-server/yarn.lock index 5312f970d3..2d30a85ceb 100644 --- a/src/rest-server/yarn.lock +++ b/src/rest-server/yarn.lock @@ -2330,6 +2330,12 @@ openid-client@2.5.0: oidc-token-hash "^3.0.1" p-any "^1.1.0" +"openpaidbsdk@file:./openpaidbsdk": + version "1.0.0" + dependencies: + pg "^8.2.1" + sequelize "5.21.3" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -2485,6 +2491,11 @@ pg-connection-string@0.1.3: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= +pg-connection-string@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6" + integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" @@ -2500,6 +2511,16 @@ pg-pool@^2.0.9: resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-2.0.9.tgz#7ed69a27e204f99e9804a851404db6aa908a6dea" integrity sha512-gNiuIEKNCT3OnudQM2kvgSnXsLkSpd6mS/fRnqs6ANtrke6j8OY5l9mnAryf1kgwJMWLg0C1N1cYTZG1xmEYHQ== +pg-pool@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0" + integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA== + +pg-protocol@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.2.5.tgz#28a1492cde11646ff2d2d06bdee42a3ba05f126c" + integrity sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg== + pg-types@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" @@ -2525,6 +2546,20 @@ pg@^7.17.1: pgpass "1.x" semver "4.3.2" +pg@^8.2.1: + version "8.3.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.3.0.tgz#941383300d38eef51ecb88a0188cec441ab64d81" + integrity sha512-jQPKWHWxbI09s/Z9aUvoTbvGgoj98AU7FDCcQ7kdejupn/TcNpx56v2gaOTzXkzOajmOEJEdi9eTh9cA2RVAjQ== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.3.0" + pg-pool "^3.2.1" + pg-protocol "^1.2.5" + pg-types "^2.1.0" + pgpass "1.x" + semver "4.3.2" + pgpass@1.x: version "1.0.2" resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" @@ -2890,7 +2925,7 @@ sequelize-pool@^2.3.0: resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.3.0.tgz#64f1fe8744228172c474f530604b6133be64993d" integrity sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA== -sequelize@^5.21.3: +sequelize@5.21.3, sequelize@^5.21.3: version "5.21.3" resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.21.3.tgz#f8a6fa0245f8995d70849e4da00c2c7c9aa9f569" integrity sha512-ptdeAxwTY0zbj7AK8m+SH3z52uHVrt/qmOTSIGo/kyfnSp3h5HeKlywkJf5GEk09kuRrPHfWARVSXH1W3IGU7g== diff --git a/src/webportal/src/app/components/util/job.js b/src/webportal/src/app/components/util/job.js index 0285893ebe..5514537b52 100644 --- a/src/webportal/src/app/components/util/job.js +++ b/src/webportal/src/app/components/util/job.js @@ -58,7 +58,8 @@ export function getHumanizedJobStateString(job) { export function getJobDuration(jobInfo) { const start = - get(jobInfo, 'createdTime') && DateTime.fromMillis(jobInfo.createdTime); + get(jobInfo, 'submissionTime') && + DateTime.fromMillis(jobInfo.submissionTime); const end = get(jobInfo, 'completedTime') && DateTime.fromMillis(jobInfo.completedTime); if (start) { diff --git a/src/webportal/src/app/job/job-view/fabric/JobList/Ordering.js b/src/webportal/src/app/job/job-view/fabric/JobList/Ordering.js index 6b26894757..4bea9f31cd 100644 --- a/src/webportal/src/app/job/job-view/fabric/JobList/Ordering.js +++ b/src/webportal/src/app/job/job-view/fabric/JobList/Ordering.js @@ -1,4 +1,4 @@ -import { getModified, getDuration, getStatusIndex } from './utils'; +import { getSubmissionTime, getDuration, getStatusIndex } from './utils'; export default class Ordering { /** @@ -20,10 +20,10 @@ export default class Ordering { comparator = descending ? (a, b) => String(b.name).localeCompare(a.name) : (a, b) => String(a.name).localeCompare(b.name); - } else if (field === 'modified') { + } else if (field === 'submissionTime') { comparator = descending - ? (a, b) => getModified(b) - getModified(a) - : (a, b) => getModified(a) - getModified(b); + ? (a, b) => getSubmissionTime(b) - getSubmissionTime(a) + : (a, b) => getSubmissionTime(a) - getSubmissionTime(b); } else if (field === 'user') { comparator = descending ? (a, b) => String(b.username).localeCompare(a.username) diff --git a/src/webportal/src/app/job/job-view/fabric/JobList/Table.jsx b/src/webportal/src/app/job/job-view/fabric/JobList/Table.jsx index ef44033794..14ab85ba21 100644 --- a/src/webportal/src/app/job/job-view/fabric/JobList/Table.jsx +++ b/src/webportal/src/app/job/job-view/fabric/JobList/Table.jsx @@ -16,7 +16,7 @@ import { import { isNil } from 'lodash'; import { DateTime } from 'luxon'; -import { getModified, getStatusText } from './utils'; +import { getSubmissionTime, getStatusText } from './utils'; import Context from './Context'; import Filter from './Filter'; import Ordering from './Ordering'; @@ -114,16 +114,16 @@ export default function Table() { }, }); const modifiedColumn = applySortProps({ - key: 'modified', + key: 'submissionTime', minWidth: 150, - name: 'Date Modified', + name: 'Submission Time', className: FontClassNames.mediumPlus, headerClassName: FontClassNames.medium, isResizable: true, - isSorted: ordering.field === 'modified', + isSorted: ordering.field === 'submissionTime', isSortedDescending: !ordering.descending, onRender(job) { - return DateTime.fromJSDate(getModified(job)).toLocaleString( + return DateTime.fromJSDate(getSubmissionTime(job)).toLocaleString( DateTime.DATETIME_SHORT_WITH_SECONDS, ); }, diff --git a/src/webportal/src/app/job/job-view/fabric/JobList/utils.js b/src/webportal/src/app/job/job-view/fabric/JobList/utils.js index 80c943aac3..4c4c761299 100644 --- a/src/webportal/src/app/job/job-view/fabric/JobList/utils.js +++ b/src/webportal/src/app/job/job-view/fabric/JobList/utils.js @@ -3,11 +3,11 @@ import { getHumanizedJobStateString } from '../../../../components/util/job'; /** * @returns {Date} */ -export function getModified(job) { - if (!('_modified' in job)) { - job._modified = new Date(job.completedTime || job.createdTime); +export function getSubmissionTime(job) { + if (!('_submissionTime' in job)) { + job._submissionTime = new Date(job.submissionTime); } - return job._modified; + return job._submissionTime; } /** @@ -15,7 +15,7 @@ export function getModified(job) { */ export function getDuration(job) { if (!('_duration' in job)) { - job._duration = (job.completedTime || Date.now()) - job.createdTime; + job._duration = (job.completedTime || Date.now()) - job.submissionTime; } return job._duration; } diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx index ddbd884d99..cdd631a0ec 100644 --- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx +++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx @@ -482,10 +482,12 @@ export default class Summary extends React.Component {
-
Start Time
+
+ Submission Time +
{printDateTime( - DateTime.fromMillis(jobInfo.jobStatus.createdTime), + DateTime.fromMillis(jobInfo.jobStatus.submissionTime), )}
From 3039d89584785559a4366994abb328e65d953adb Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Wed, 22 Jul 2020 22:34:29 +0800 Subject: [PATCH 02/32] fix --- .github/workflows/continuous-integration.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 81e800b059..990c8b717c 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -131,8 +131,10 @@ jobs: - name: yarn install and test run: | cd src/rest-server + cp -rf ../database-controller/sdk openpaidbsdk yarn config set ignore-engines true yarn install --frozen-lockfiles + rm -rf openpaidbsdk yarn test webportal: From eec3e8037600ac8c75695ebe6cf7dac06bf2c4ca Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Wed, 22 Jul 2020 22:41:47 +0800 Subject: [PATCH 03/32] fix --- src/rest-server/text | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 src/rest-server/text diff --git a/src/rest-server/text b/src/rest-server/text deleted file mode 100644 index ba2a34d806..0000000000 --- a/src/rest-server/text +++ /dev/null @@ -1,17 +0,0 @@ - -> pai-rest-server@0.0.0 start C:\Users\zhiyuhe\project\pai\src\rest-server -> node index.js - -2020-06-30T04:07:47.240Z [INFO] Create admin group configured in configuration. -2020-06-30T04:07:47.968Z [INFO] config: {"env":"production","logLevel":"debug","serverPort":8080,"jwtSecret":"pai-secret"} -2020-06-30T04:07:47.985Z [INFO] RESTful API server starts on port 8080 -2020-06-30T04:07:49.013Z [INFO] connected to framework controller successfully -2020-06-30T04:07:49.016Z [INFO] Namespace pai-user-token already exists -2020-06-30T04:07:49.045Z [INFO] Create admin group successfully. -2020-06-30T04:07:49.045Z [INFO] create default vc's group. -2020-06-30T04:07:50.087Z [INFO] Create default group successfully. -2020-06-30T04:07:50.087Z [INFO] Create group configured in configuration. -2020-06-30T04:07:50.088Z [INFO] Create group successfully. -2020-06-30T04:07:51.634Z [INFO] Create admin user account configured in configuration. -2020-06-30T04:07:52.655Z [INFO] Create admin user account successfully. -Terminate batch job (Y/N)? From 429dc4edf5e651793dd72a3e33f1ab0f42d2c75d Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 23 Jul 2020 14:26:35 +0800 Subject: [PATCH 04/32] trigger From dbe94ac6a5f8ff891a9c683f89171be9d6169035 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 23 Jul 2020 14:57:17 +0800 Subject: [PATCH 05/32] fix --- src/database-controller/build/build-pre.sh | 2 +- src/rest-server/build/build-pre.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database-controller/build/build-pre.sh b/src/database-controller/build/build-pre.sh index e1e0054cf6..2eedb0201c 100644 --- a/src/database-controller/build/build-pre.sh +++ b/src/database-controller/build/build-pre.sh @@ -20,7 +20,7 @@ pushd $(dirname "$0") > /dev/null mkdir -p ../version -cp ../../../version/PAI.VERSION ../version/ +cp -af ../../../version/PAI.VERSION ../version/ echo `git rev-parse HEAD` > ../version/COMMIT.VERSION popd > /dev/null diff --git a/src/rest-server/build/build-pre.sh b/src/rest-server/build/build-pre.sh index 5502e27d6b..68737c73a8 100755 --- a/src/rest-server/build/build-pre.sh +++ b/src/rest-server/build/build-pre.sh @@ -20,6 +20,6 @@ pushd $(dirname "$0") > /dev/null cp -arf "../../../version" "../version" -cp -rf ../../database-controller/sdk ../openpaidbsdk +cp -arf ../../database-controller/sdk ../openpaidbsdk popd > /dev/null From c2adce5ca330cabbf3a8ff81a4327a901a89393c Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 23 Jul 2020 16:45:17 +0800 Subject: [PATCH 06/32] fix --- src/database-controller/build/build-pre.sh | 6 +++--- src/rest-server/build/build-pre.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/database-controller/build/build-pre.sh b/src/database-controller/build/build-pre.sh index 2eedb0201c..2cd8bc849a 100644 --- a/src/database-controller/build/build-pre.sh +++ b/src/database-controller/build/build-pre.sh @@ -19,8 +19,8 @@ pushd $(dirname "$0") > /dev/null -mkdir -p ../version -cp -af ../../../version/PAI.VERSION ../version/ -echo `git rev-parse HEAD` > ../version/COMMIT.VERSION +mkdir -p "../version" +cp -af "../../../version/PAI.VERSION" "../version/" +echo `git rev-parse HEAD` > "../version/COMMIT.VERSION" popd > /dev/null diff --git a/src/rest-server/build/build-pre.sh b/src/rest-server/build/build-pre.sh index 68737c73a8..e989221a37 100755 --- a/src/rest-server/build/build-pre.sh +++ b/src/rest-server/build/build-pre.sh @@ -20,6 +20,6 @@ pushd $(dirname "$0") > /dev/null cp -arf "../../../version" "../version" -cp -arf ../../database-controller/sdk ../openpaidbsdk +cp -arf "../../database-controller/sdk" "../openpaidbsdk" popd > /dev/null From 642c868d2e279e09e18b3c98e3dfe0a178e18cf7 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 23 Jul 2020 16:54:48 +0800 Subject: [PATCH 07/32] fix --- src/database-controller/build/build-pre.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database-controller/build/build-pre.sh b/src/database-controller/build/build-pre.sh index 2cd8bc849a..e1b6f93f6d 100644 --- a/src/database-controller/build/build-pre.sh +++ b/src/database-controller/build/build-pre.sh @@ -19,8 +19,8 @@ pushd $(dirname "$0") > /dev/null -mkdir -p "../version" -cp -af "../../../version/PAI.VERSION" "../version/" +mkdir -m 766 -p "../version" +cp -arf "../../../version/PAI.VERSION" "../version/" echo `git rev-parse HEAD` > "../version/COMMIT.VERSION" popd > /dev/null From e46f0acc544c80445231d42fac0f9c4401ec89e2 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 23 Jul 2020 17:01:01 +0800 Subject: [PATCH 08/32] fix --- src/database-controller/build/build-pre.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database-controller/build/build-pre.sh b/src/database-controller/build/build-pre.sh index e1b6f93f6d..9f7433d38d 100644 --- a/src/database-controller/build/build-pre.sh +++ b/src/database-controller/build/build-pre.sh @@ -19,7 +19,7 @@ pushd $(dirname "$0") > /dev/null -mkdir -m 766 -p "../version" +mkdir -m 777 -p "../version" cp -arf "../../../version/PAI.VERSION" "../version/" echo `git rev-parse HEAD` > "../version/COMMIT.VERSION" From c6900f612033f2ed620b3c05ce61df0684004662 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 23 Jul 2020 17:29:53 +0800 Subject: [PATCH 09/32] fix --- src/database-controller/sdk/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/database-controller/sdk/index.js b/src/database-controller/sdk/index.js index a989bd587a..6868f9c75e 100644 --- a/src/database-controller/sdk/index.js +++ b/src/database-controller/sdk/index.js @@ -236,7 +236,11 @@ class DatabaseModel { } async getVersion () { - const res = await this.Version.findOne() + let res + try { + res = await this.Version.findOne() + } catch (err) { + } if (res) { return { version: res.version, From e4e7e7229c25744c256365b6e5848641def4bbaf Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Fri, 24 Jul 2020 17:35:55 +0800 Subject: [PATCH 10/32] fix --- src/rest-server/docs/swagger.yaml | 4 ++-- src/rest-server/src/controllers/v2/job.js | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/rest-server/docs/swagger.yaml b/src/rest-server/docs/swagger.yaml index c291aff64f..5408d2f871 100644 --- a/src/rest-server/docs/swagger.yaml +++ b/src/rest-server/docs/swagger.yaml @@ -1097,7 +1097,7 @@ paths: security: - bearerAuth: [] parameters: - - name: userName + - name: username in: query description: filter jobs with username schema: @@ -1129,7 +1129,7 @@ paths: type: number - name: order in: query - description: 'order of job list. It follows the format ,, default value is "submissionTime,DESC". Available fields include: jobName, submissionTime, userName, vc, retries, totalTaskNumber, totalGpuNumber, state' + description: 'order of job list. It follows the format ,, default value is "submissionTime,DESC". Available fields include: jobName, submissionTime, username, vc, retries, totalTaskNumber, totalGpuNumber, state' schema: type: string - name: withTotalCount diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index ad60273bab..7244f6f5e9 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -26,7 +26,7 @@ const {Op} = require('sequelize'); const list = asyncHandler(async (req, res) => { - // ?keyword=&userName=,&vc=, + // ?keyword=&username=,&vc=, // &state=,&offset=&limit=&withTotalCount=true // &order=state,DESC const filters = {}; @@ -35,8 +35,8 @@ const list = asyncHandler(async (req, res) => { let withTotalCount = false; let order = []; if (req.query) { - if ('userName' in req.query) { - filters.userName = req.query.userName.split(','); + if ('username' in req.query) { + filters.userName = req.query.username.split(','); } if ('vc' in req.query) { filters.virtualCluster = req.query.vc.split(','); @@ -63,10 +63,15 @@ const list = asyncHandler(async (req, res) => { } if ('order' in req.query) { const {field, ordering} = req.query.order.split(','); - if (['jobName', 'submissionTime', 'userName', 'vc', 'retries', 'totalTaskNumber', + if (['jobName', 'submissionTime', 'username', 'vc', 'retries', 'totalTaskNumber', 'totalGpuNumber', 'state'].includes(field)) { if (ordering === 'ASC' || ordering === 'DESC') { - order.push([field, ordering]); + // different cases for username + if (field !== 'username') { + order.push([field, ordering]); + } else { + order.push(['userName', ordering]); + } } } } From 4a64f655cc11367846c27256f5b14c3fb8aba8c1 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Wed, 29 Jul 2020 11:39:38 +0800 Subject: [PATCH 11/32] fix rbac --- .../deploy/database-controller.yaml.template | 27 ++++++++++ .../deploy/database-initializer.yaml.template | 9 ++++ src/database-controller/src/core/config.js | 5 +- src/database-controller/src/core/k8s.js | 52 +++++++++++-------- .../src/initializer/index.js | 1 - 5 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/database-controller/deploy/database-controller.yaml.template b/src/database-controller/deploy/database-controller.yaml.template index ae4558bbd9..106fc33a4c 100644 --- a/src/database-controller/deploy/database-controller.yaml.template +++ b/src/database-controller/deploy/database-controller.yaml.template @@ -50,6 +50,15 @@ spec: value: "true" {% else %} value: "false" +{% endif %} +{% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} + - name: RBAC_IN_CLUSTER + value: "true" +{% else %} + - name: RBAC_IN_CLUSTER + value: "false" + - name: CUSTOM_K8S_API_SERVER_URL + value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} - name: DB_CONNECTION_STR value: "{{ cluster_cfg['postgresql']['connection-str'] }}" @@ -83,6 +92,15 @@ spec: value: "true" {% else %} value: "false" +{% endif %} +{% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} + - name: RBAC_IN_CLUSTER + value: "true" +{% else %} + - name: RBAC_IN_CLUSTER + value: "false" + - name: CUSTOM_K8S_API_SERVER_URL + value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} - name: WRITE_MERGER_URL value: "{{ cluster_cfg['database-controller']['write-merger-url'] }}" @@ -102,6 +120,15 @@ spec: value: "true" {% else %} value: "false" +{% endif %} +{% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} + - name: RBAC_IN_CLUSTER + value: "true" +{% else %} + - name: RBAC_IN_CLUSTER + value: "false" + - name: CUSTOM_K8S_API_SERVER_URL + value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} - name: DB_CONNECTION_STR value: "{{ cluster_cfg['postgresql']['connection-str'] }}" diff --git a/src/database-controller/deploy/database-initializer.yaml.template b/src/database-controller/deploy/database-initializer.yaml.template index 3186db0ab6..37b4504488 100644 --- a/src/database-controller/deploy/database-initializer.yaml.template +++ b/src/database-controller/deploy/database-initializer.yaml.template @@ -48,6 +48,15 @@ spec: value: "true" {% else %} value: "false" +{% endif %} +{% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} + - name: RBAC_IN_CLUSTER + value: "true" +{% else %} + - name: RBAC_IN_CLUSTER + value: "false" + - name: CUSTOM_K8S_API_SERVER_URL + value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} - name: DB_CONNECTION_STR value: "{{ cluster_cfg['postgresql']['connection-str'] }}" diff --git a/src/database-controller/src/core/config.js b/src/database-controller/src/core/config.js index b90fda23eb..bbe733d210 100644 --- a/src/database-controller/src/core/config.js +++ b/src/database-controller/src/core/config.js @@ -35,6 +35,8 @@ const configSchema = Joi.object().keys({ customK8sTokenFile: Joi.string() .optional(), recoveryModeEnabled: Joi.boolean() + .required(), + rbacEnabled: Joi.boolean() .required() }).required() @@ -45,7 +47,8 @@ const config = { customK8sApiServerURL: process.env.CUSTOM_K8S_API_SERVER_URL, customK8sCaFile: process.env.CUSTOM_K8S_CA_FILE, customK8sTokenFile: process.env.CUSTOM_K8S_TOKEN_FILE, - recoveryModeEnabled: process.env.RECOVERY_MODE_ENABLED === 'true' + recoveryModeEnabled: process.env.RECOVERY_MODE_ENABLED === 'true', + rbacEnabled: process.env.RBAC_IN_CLUSTER === 'true' } const { error, value } = Joi.validate(config, configSchema) diff --git a/src/database-controller/src/core/k8s.js b/src/database-controller/src/core/k8s.js index 3ca7f4091a..cd76cea91c 100644 --- a/src/database-controller/src/core/k8s.js +++ b/src/database-controller/src/core/k8s.js @@ -21,32 +21,40 @@ const config = require('./config') const { timeoutDecorator } = require('./util') const kc = new k8s.KubeConfig() -if (config.customK8sApiServerURL) { - // For local debugging, one should set CUSTOM_K8S_API_SERVER_URL, CUSTOM_K8S_CA_FILE and CUSTOM_K8S_TOKEN_FILE. - const cluster = { - name: 'inCluster', - caFile: config.customK8sCaFile, - server: config.customK8sApiServerURL, - skipTLSVerify: false +if (config.rbacEnabled) { + // If RBAC is enabled, we can use kc.loadFromDefault() to load k8s config in containers. + // For local debugging purpose, one can set CUSTOM_K8S_API_SERVER_URL, CUSTOM_K8S_CA_FILE and CUSTOM_K8S_TOKEN_FILE, + // to connect to RBAC k8s cluster. Ca and Token file should be from a valid service account. + if (config.customK8sApiServerURL) { + const cluster = { + name: 'inCluster', + caFile: config.customK8sCaFile, + server: config.customK8sApiServerURL, + skipTLSVerify: false + } + const user = { + name: 'inClusterUser', + authProvider: + { + name: 'tokenFile', + config: + { + tokenFile: config.customK8sTokenFile + } + } + } + kc.loadFromClusterAndUser(cluster, user) + } else { + kc.loadFromDefault() } - const user = { - name: 'inClusterUser', - authProvider: - { - name: 'tokenFile', - config: - { - tokenFile: config.customK8sTokenFile - } - } - } - kc.loadFromClusterAndUser(cluster, user) } else { - // For in-cluster containers, load KubeConfig from default setting. - kc.loadFromDefault() + // If RBAC is not enabled, use CUSTOM_K8S_API_SERVER_URL to connect to API server. + const cluster = { name: 'cluster', server: config.customK8sApiServerURL } + const user = { name: 'user' } + kc.loadFromClusterAndUser(cluster, user) } -// If api server reports a non-200 status code, the client will +// If API server reports a non-200 status code, the client will // throw an error. In such case, error.name will be "HttpError", // and error.response.statusCode is the actual status code. // If network is disconnected, the error will be a different one, diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index 59fc7d1aef..96ae3d9eb2 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -30,7 +30,6 @@ async function updateFromNoDatabaseVersion (databaseModel) { await databaseModel.synchronizeSchema() // transfer old frameworks from api server to db const frameworks = (await k8s.listFramework()).body.items - console.log(frameworks) for (const framework of frameworks) { const snapshot = new Snapshot(framework) logger.info(`Transferring framework ${snapshot.getName()} to database.`) From 3850a1099291e42c18e74cf3fbfc9485c5f8e012 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Wed, 29 Jul 2020 14:07:16 +0800 Subject: [PATCH 12/32] fix --- src/rest-server/docs/swagger.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rest-server/docs/swagger.yaml b/src/rest-server/docs/swagger.yaml index 5408d2f871..dac94ef3ac 100644 --- a/src/rest-server/docs/swagger.yaml +++ b/src/rest-server/docs/swagger.yaml @@ -1873,6 +1873,7 @@ components: description: job submitted time, in number of milliseconds since the Unix Epoch. createdTime: type: integer + nullable: true description: job created time, in number of milliseconds since the Unix Epoch. completedTime: type: integer From a3e2cab22a4d7ccd87b806f0f8c23e538857a1a7 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 3 Aug 2020 11:41:33 +0800 Subject: [PATCH 13/32] change rest-server & initializer --- .../deploy/database-controller.yaml.template | 29 ++++++++ .../deploy/database-initializer.yaml.template | 70 ------------------- .../deploy/start.sh.template | 8 +-- src/database-controller/deploy/stop.sh | 3 - .../src/initializer/index.js | 16 ++--- src/rest-server/docs/swagger.yaml | 2 +- src/rest-server/src/controllers/v2/job.js | 9 ++- 7 files changed, 43 insertions(+), 94 deletions(-) delete mode 100644 src/database-controller/deploy/database-initializer.yaml.template diff --git a/src/database-controller/deploy/database-controller.yaml.template b/src/database-controller/deploy/database-controller.yaml.template index 106fc33a4c..7622d08e9e 100644 --- a/src/database-controller/deploy/database-controller.yaml.template +++ b/src/database-controller/deploy/database-controller.yaml.template @@ -34,6 +34,35 @@ spec: spec: serviceAccountName: database-controller-account hostNetwork: true + initContainers: + - name: database-initializer + image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} + imagePullPolicy: Always + env: + - name: LOG_LEVEL + value: "{{ cluster_cfg['database-controller']['log-level'] }}" + - name: K8S_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND + value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + - name: RECOVERY_MODE_ENABLED +{% if cluster_cfg['database-controller']['recovery-mode'] %} + value: "true" +{% else %} + value: "false" +{% endif %} +{% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} + - name: RBAC_IN_CLUSTER + value: "true" +{% else %} + - name: RBAC_IN_CLUSTER + value: "false" + - name: CUSTOM_K8S_API_SERVER_URL + value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} +{% endif %} + - name: DB_CONNECTION_STR + value: "{{ cluster_cfg['postgresql']['connection-str'] }}" + command: ["node", "initializer/index.js"] containers: - name: write-merger image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} diff --git a/src/database-controller/deploy/database-initializer.yaml.template b/src/database-controller/deploy/database-initializer.yaml.template deleted file mode 100644 index 37b4504488..0000000000 --- a/src/database-controller/deploy/database-initializer.yaml.template +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: database-initializer-sts -spec: - selector: - matchLabels: - app: database-initializer - serviceName: database-initializer - replicas: 1 - template: - metadata: - labels: - app: database-initializer - spec: - serviceAccountName: database-controller-account - hostNetwork: true - containers: - - name: database-initializer - image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} - imagePullPolicy: Always - env: - - name: LOG_LEVEL - value: "{{ cluster_cfg['database-controller']['log-level'] }}" - - name: K8S_CONNECTION_TIMEOUT_SECOND - value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" - - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND - value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" - - name: RECOVERY_MODE_ENABLED -{% if cluster_cfg['database-controller']['recovery-mode'] %} - value: "true" -{% else %} - value: "false" -{% endif %} -{% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} - - name: RBAC_IN_CLUSTER - value: "true" -{% else %} - - name: RBAC_IN_CLUSTER - value: "false" - - name: CUSTOM_K8S_API_SERVER_URL - value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} -{% endif %} - - name: DB_CONNECTION_STR - value: "{{ cluster_cfg['postgresql']['connection-str'] }}" - command: ["node", "initializer/index.js"] - readinessProbe: - exec: - command: - - ls - - /READY - imagePullSecrets: - - name: {{ cluster_cfg["cluster"]["docker-registry"]["secret-name"] }} diff --git a/src/database-controller/deploy/start.sh.template b/src/database-controller/deploy/start.sh.template index 4be1562eda..64f32237ef 100644 --- a/src/database-controller/deploy/start.sh.template +++ b/src/database-controller/deploy/start.sh.template @@ -21,14 +21,8 @@ pushd $(dirname "$0") > /dev/null kubectl apply --overwrite=true -f rbac.yaml || exit $? -kubectl apply --overwrite=true -f database-initializer.yaml || exit $? -echo 'Database is being initialized, please wait...' -PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ready_status -w -k app -v database-initializer || exit $? -PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.maintaintool.update_resource \ - --operation delete --resource statefulset --name database-initializer-sts - - kubectl apply --overwrite=true -f database-controller.yaml || exit $? + # Wait until the service is ready. PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.monitorTool.check_pod_ready_status -w -k app -v database-controller || exit $? diff --git a/src/database-controller/deploy/stop.sh b/src/database-controller/deploy/stop.sh index 881287903d..c67cdcef31 100644 --- a/src/database-controller/deploy/stop.sh +++ b/src/database-controller/deploy/stop.sh @@ -19,9 +19,6 @@ pushd $(dirname "$0") > /dev/null -PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.maintaintool.update_resource \ - --operation delete --resource statefulset --name database-initializer-sts - PYTHONPATH="../../../deployment" python -m k8sPaiLibrary.maintaintool.update_resource \ --operation delete --resource statefulset --name database-controller-sts diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index 96ae3d9eb2..0a3ab64449 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -20,7 +20,6 @@ require('dotenv').config() const DatabaseModel = require('openpaidbsdk') const fs = require('fs') const logger = require('@dbc/core/logger') -const neverResolved = new Promise((resolve, reject) => {}) const { paiVersion, paiCommitVersion } = require('@dbc/package.json') const k8s = require('@dbc/core/k8s') const { Snapshot } = require('@dbc/core/framework') @@ -37,9 +36,11 @@ async function updateFromNoDatabaseVersion (databaseModel) { record.requestSynced = true await databaseModel.Framework.upsert(record) } - // TO DO: transfer old framework history from api server to db } +// This script should be idempotent. +// If any error happens, it should report the error and exit with a non-zero code. +// If succeed, it should finish with a zero code. async function main () { try { const databaseModel = new DatabaseModel( @@ -51,20 +52,11 @@ async function main () { await updateFromNoDatabaseVersion(databaseModel) } await databaseModel.setVersion(paiVersion, paiCommitVersion) - await new Promise((resolve, reject) => { - fs.writeFile('/READY', '', (err) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) logger.info('Database has been successfully initialized.') } catch (err) { logger.error(err) + process.exit(1) } - await neverResolved // sleep forever } main() diff --git a/src/rest-server/docs/swagger.yaml b/src/rest-server/docs/swagger.yaml index dac94ef3ac..f869c84552 100644 --- a/src/rest-server/docs/swagger.yaml +++ b/src/rest-server/docs/swagger.yaml @@ -1124,7 +1124,7 @@ paths: type: number - name: limit in: query - description: list job limit + description: list job limit. It has a default number 5000, and its max number is 50000. schema: type: number - name: order diff --git a/src/rest-server/src/controllers/v2/job.js b/src/rest-server/src/controllers/v2/job.js index 7244f6f5e9..2d930247c0 100644 --- a/src/rest-server/src/controllers/v2/job.js +++ b/src/rest-server/src/controllers/v2/job.js @@ -24,7 +24,6 @@ const createError = require('@pai/utils/error'); const job = require('@pai/models/v2/job'); const {Op} = require('sequelize'); - const list = asyncHandler(async (req, res) => { // ?keyword=&username=,&vc=, // &state=,&offset=&limit=&withTotalCount=true @@ -34,6 +33,9 @@ const list = asyncHandler(async (req, res) => { let limit; let withTotalCount = false; let order = []; + // limit has a max number and a default number + const maxLimit = 50000; + const defaultLimit = 5000; if (req.query) { if ('username' in req.query) { filters.userName = req.query.username.split(','); @@ -49,6 +51,11 @@ const list = asyncHandler(async (req, res) => { } if ('limit' in req.query) { limit = parseInt(req.query.limit); + if (limit > maxLimit) { + throw createError('Bad Request', 'InvalidParametersError', `Limit exceeds max number ${maxLimit}.`); + } + } else { + limit = defaultLimit; } if ('withTotalCount' in req.query && req.query.withTotalCount === 'true') { withTotalCount = true; From d2ed44808db332d69e29dc1beb31820cb46584d6 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 3 Aug 2020 11:46:14 +0800 Subject: [PATCH 14/32] fix --- src/database-controller/deploy/service.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/database-controller/deploy/service.yaml b/src/database-controller/deploy/service.yaml index b3bc1d10ea..06f404bc26 100644 --- a/src/database-controller/deploy/service.yaml +++ b/src/database-controller/deploy/service.yaml @@ -22,7 +22,6 @@ prerequisite: - postgresql template-list: - - database-initializer.yaml - database-controller.yaml - start.sh From 197ca829255c4f13f922330e1f8cb361904ba0e4 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 3 Aug 2020 11:57:38 +0800 Subject: [PATCH 15/32] fix --- src/database-controller/src/initializer/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index 0a3ab64449..6ae860f48b 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -53,6 +53,7 @@ async function main () { } await databaseModel.setVersion(paiVersion, paiCommitVersion) logger.info('Database has been successfully initialized.') + process.exit(0) } catch (err) { logger.error(err) process.exit(1) From d235d4ed456d0663ac2edff5603ccaa25b65186f Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 3 Aug 2020 13:22:09 +0800 Subject: [PATCH 16/32] fix --- src/rest-server/docs/swagger.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rest-server/docs/swagger.yaml b/src/rest-server/docs/swagger.yaml index f869c84552..ce21035dc6 100644 --- a/src/rest-server/docs/swagger.yaml +++ b/src/rest-server/docs/swagger.yaml @@ -1976,6 +1976,7 @@ components: Epoch. createdTime: type: integer + nullable: true description: >- job created time, in number of milliseconds since the Unix Epoch. From 1cb37d013a6f67c0da9e7862a277109019218507 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 3 Aug 2020 13:58:15 +0800 Subject: [PATCH 17/32] fix --- src/database-controller/src/core/framework.js | 12 ++++++++++ src/database-controller/src/core/k8s.js | 1 + src/database-controller/src/index.js | 23 ------------------- src/database-controller/src/poller/index.js | 5 ++++ .../src/watcher/framework/index.js | 1 + src/rest-server/docs/swagger.yaml | 1 + 6 files changed, 20 insertions(+), 23 deletions(-) delete mode 100644 src/database-controller/src/index.js diff --git a/src/database-controller/src/core/framework.js b/src/database-controller/src/core/framework.js index c784cb0272..108e4dfa14 100644 --- a/src/database-controller/src/core/framework.js +++ b/src/database-controller/src/core/framework.js @@ -74,6 +74,12 @@ function ignoreError (err) { logger.info('This error will be ignored: ', err) } +// Class `Snapshot` handles the full json of framework. +// It provides method like: +// getRequest: extract framework request from the full json +// overrideRequest: override the framework request using another snapshot +// getRequestUpdate, getStatusUpdate, getAllUpdate: Get database updates from the snapshot. +// It doesn't handle database internal status, like: requestSynced, apiServerDeleted, ..., etc. class Snapshot { constructor (snapshot) { if (snapshot instanceof Object) { @@ -81,6 +87,8 @@ class Snapshot { } else { this._snapshot = JSON.parse(snapshot) } + // If the snapshot doesn't have a status, mock one instead. + // This usually happens when the framework spec is generated by rest-server. if (!this._snapshot.status) { this._snapshot.status = mockFrameworkStatus() } @@ -228,6 +236,8 @@ class Snapshot { } } +// Class Add-ons handles creation/patching/deletion of job add-ons. +// Currently there are 3 types of add-ons: configSecret, priorityClass, and dockerSecret. class AddOns { constructor (configSecretDef = null, priorityClassDef = null, dockerSecretDef = null) { if (configSecretDef !== null && !(configSecretDef instanceof Object)) { @@ -317,6 +327,7 @@ async function synchronizeCreate (snapshot, addOns) { const response = await k8s.createFramework(snapshot.getRequest(false)) // framework is created successfully. const frameworkResponse = response.body + // don't wait for patching addOns.silentPatch(frameworkResponse) return frameworkResponse } catch (err) { @@ -342,6 +353,7 @@ async function synchronizeRequest (snapshot, addOns) { // any error will be raised // if succeed, return framework from api server // There may be multiple calls of synchronizeRequest. + // Poller and write-merger uses this method. try { await k8s.getFramework(snapshot.getName()) // if framework exists diff --git a/src/database-controller/src/core/k8s.js b/src/database-controller/src/core/k8s.js index cd76cea91c..7ad48c86cf 100644 --- a/src/database-controller/src/core/k8s.js +++ b/src/database-controller/src/core/k8s.js @@ -200,6 +200,7 @@ async function patchSecretOwnerToFramework (secret, frameworkResponse) { const timeoutMs = config.k8sConnectionTimeoutSecond * 1000 +// We give every method a timeout. module.exports = { getFramework: timeoutDecorator(getFramework, 'Kubernetes getFramework', timeoutMs), listFramework: timeoutDecorator(listFramework, 'Kubernetes getFramework', timeoutMs), diff --git a/src/database-controller/src/index.js b/src/database-controller/src/index.js deleted file mode 100644 index a175ab6e09..0000000000 --- a/src/database-controller/src/index.js +++ /dev/null @@ -1,23 +0,0 @@ -require('module-alias/register') -require('dotenv').config() -const k8s = require('@dbc/core/k8s') -const logger = require('@dbc/core/logger') - -async function main () { - let res - res = await k8s.createSecret( - 'test', - { 'test-key': Buffer.from('test-value').toString('base64') } - ) - logger.info(res) - res = await k8s.patchSecretOwnerToFramework( - 'test', - 'e76bcfa99de735b3e16c2470f5e7ca2b', - '865fc53f-50f8-4c25-9a62-18bab47063aa' - ) - logger.info(res) - res = await k8s.deleteSecret('test') - logger.info(res) -} - -main() diff --git a/src/database-controller/src/poller/index.js b/src/database-controller/src/poller/index.js index 7a6ff016f8..1a3175a28a 100644 --- a/src/database-controller/src/poller/index.js +++ b/src/database-controller/src/poller/index.js @@ -27,6 +27,8 @@ const interval = require('interval-promise') const config = require('@dbc/poller/config') const fetch = require('node-fetch') const { deleteFramework } = require('@dbc/core/k8s') +// maxPending is set to 1 to avoid the queue to be too long. +// If any framework is not synced/deleted, it will be synced/deleted in the next polling round. const lock = new AsyncLock({ maxPending: 1 }) const databaseModel = new DatabaseModel( config.dbConnectionStr, @@ -51,6 +53,9 @@ function deleteHandler (snapshot, pollingTs) { lock.acquire(frameworkName, async () => { try { + // We only delete framework here, ignoring the job add-ons. + // Because most job add-ons are patched in the creation time, so they will by deleted automatically. + // If some add-ons are not successfully patched, they will be deleted by the watch dog service. await deleteFramework(snapshot.getName()) logger.info(`Framework ${frameworkName} is successfully deleted. PollingTs=${pollingTs}.`) } catch (err) { diff --git a/src/database-controller/src/watcher/framework/index.js b/src/database-controller/src/watcher/framework/index.js index 30d08c18ca..b3346738c7 100644 --- a/src/database-controller/src/watcher/framework/index.js +++ b/src/database-controller/src/watcher/framework/index.js @@ -63,6 +63,7 @@ informer.on('add', (apiObject) => { eventHandler('ADDED', apiObject) }) informer.on('update', (apiObject) => { eventHandler('MODIFED', apiObject) }) informer.on('delete', (apiObject) => { eventHandler('DELETED', apiObject) }) informer.on('error', (err) => { + // If any error happens, the process should exit, and let Kubernetes restart it. logger.error(err) process.exit(1) }) diff --git a/src/rest-server/docs/swagger.yaml b/src/rest-server/docs/swagger.yaml index ce21035dc6..b40183c5f8 100644 --- a/src/rest-server/docs/swagger.yaml +++ b/src/rest-server/docs/swagger.yaml @@ -2436,6 +2436,7 @@ components: nullable: true jobStartedTime: type: integer + nullable: true attemptStartedTime: type: integer nullable: true From 54d219fc678fb3a32b1c006d2ba13293f05c88ba Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 3 Aug 2020 14:53:15 +0800 Subject: [PATCH 18/32] change license comment --- src/database-controller/build/build-pre.sh | 18 ++--------------- .../database-controller.common.dockerfile | 18 ++--------------- .../config/database-controller.yaml | 18 ++--------------- .../config/database_controller.py | 20 +++---------------- .../deploy/database-controller.yaml.template | 18 ++--------------- src/database-controller/deploy/delete.sh | 20 +++---------------- src/database-controller/deploy/rbac.yaml | 18 ++--------------- src/database-controller/deploy/refresh.sh | 19 ++---------------- src/database-controller/deploy/service.yaml | 18 ++--------------- .../deploy/start.sh.template | 18 ++--------------- src/database-controller/deploy/stop.sh | 18 ++--------------- src/database-controller/sdk/index.js | 3 +++ src/database-controller/src/core/config.js | 18 ++--------------- src/database-controller/src/core/framework.js | 18 ++--------------- src/database-controller/src/core/k8s.js | 18 ++--------------- src/database-controller/src/core/logger.js | 18 ++--------------- src/database-controller/src/core/util.js | 18 ++--------------- .../src/initializer/index.js | 18 ++--------------- src/database-controller/src/poller/config.js | 18 ++--------------- src/database-controller/src/poller/index.js | 18 ++--------------- .../src/watcher/framework/config.js | 18 ++--------------- .../src/watcher/framework/index.js | 18 ++--------------- .../src/write-merger/app.js | 18 ++--------------- .../src/write-merger/config.js | 18 ++--------------- .../src/write-merger/handler.js | 18 ++--------------- .../src/write-merger/index.js | 18 ++--------------- 26 files changed, 55 insertions(+), 403 deletions(-) diff --git a/src/database-controller/build/build-pre.sh b/src/database-controller/build/build-pre.sh index 9f7433d38d..ed004c1d39 100644 --- a/src/database-controller/build/build-pre.sh +++ b/src/database-controller/build/build-pre.sh @@ -1,21 +1,7 @@ #!/bin/bash -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. pushd $(dirname "$0") > /dev/null diff --git a/src/database-controller/build/database-controller.common.dockerfile b/src/database-controller/build/database-controller.common.dockerfile index 46c72bbc0d..5d954b9f88 100644 --- a/src/database-controller/build/database-controller.common.dockerfile +++ b/src/database-controller/build/database-controller.common.dockerfile @@ -1,19 +1,5 @@ -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. FROM node:carbon diff --git a/src/database-controller/config/database-controller.yaml b/src/database-controller/config/database-controller.yaml index 0445c21113..37b1a1c7cb 100644 --- a/src/database-controller/config/database-controller.yaml +++ b/src/database-controller/config/database-controller.yaml @@ -1,19 +1,5 @@ -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. service_type: "common" diff --git a/src/database-controller/config/database_controller.py b/src/database-controller/config/database_controller.py index 5bbb96d866..17b542b369 100644 --- a/src/database-controller/config/database_controller.py +++ b/src/database-controller/config/database_controller.py @@ -1,21 +1,7 @@ #!/usr/bin/env python -# -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. import copy import logging diff --git a/src/database-controller/deploy/database-controller.yaml.template b/src/database-controller/deploy/database-controller.yaml.template index 7622d08e9e..b609f2f025 100644 --- a/src/database-controller/deploy/database-controller.yaml.template +++ b/src/database-controller/deploy/database-controller.yaml.template @@ -1,21 +1,7 @@ #!/bin/bash -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. apiVersion: apps/v1 kind: StatefulSet diff --git a/src/database-controller/deploy/delete.sh b/src/database-controller/deploy/delete.sh index 1b952d11cc..a461ca1401 100644 --- a/src/database-controller/deploy/delete.sh +++ b/src/database-controller/deploy/delete.sh @@ -1,21 +1,7 @@ #!/bin/bash -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. pushd $(dirname "$0") > /dev/null @@ -23,4 +9,4 @@ echo "Call stop script to stop all service first" /bin/bash stop.sh || exit $? -popd > /dev/null \ No newline at end of file +popd > /dev/null diff --git a/src/database-controller/deploy/rbac.yaml b/src/database-controller/deploy/rbac.yaml index 52cd13471f..42063f85a0 100644 --- a/src/database-controller/deploy/rbac.yaml +++ b/src/database-controller/deploy/rbac.yaml @@ -1,19 +1,5 @@ -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. apiVersion: v1 kind: ServiceAccount diff --git a/src/database-controller/deploy/refresh.sh b/src/database-controller/deploy/refresh.sh index 3b6d2ca17d..bd50ad3ded 100644 --- a/src/database-controller/deploy/refresh.sh +++ b/src/database-controller/deploy/refresh.sh @@ -1,22 +1,7 @@ #!/bin/bash -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. pushd $(dirname "$0") > /dev/null diff --git a/src/database-controller/deploy/service.yaml b/src/database-controller/deploy/service.yaml index 06f404bc26..1024503295 100644 --- a/src/database-controller/deploy/service.yaml +++ b/src/database-controller/deploy/service.yaml @@ -1,19 +1,5 @@ -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. cluster-type: - k8s diff --git a/src/database-controller/deploy/start.sh.template b/src/database-controller/deploy/start.sh.template index 64f32237ef..8e256b3e47 100644 --- a/src/database-controller/deploy/start.sh.template +++ b/src/database-controller/deploy/start.sh.template @@ -1,21 +1,7 @@ #!/bin/bash -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. pushd $(dirname "$0") > /dev/null diff --git a/src/database-controller/deploy/stop.sh b/src/database-controller/deploy/stop.sh index c67cdcef31..a32e556404 100644 --- a/src/database-controller/deploy/stop.sh +++ b/src/database-controller/deploy/stop.sh @@ -1,21 +1,7 @@ #!/bin/bash -# Copyright (c) Microsoft Corporation -# All rights reserved. -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -# to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. pushd $(dirname "$0") > /dev/null diff --git a/src/database-controller/sdk/index.js b/src/database-controller/sdk/index.js index 6868f9c75e..8be0deae2b 100644 --- a/src/database-controller/sdk/index.js +++ b/src/database-controller/sdk/index.js @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + const { Sequelize, Model } = require('sequelize') class DatabaseModel { diff --git a/src/database-controller/src/core/config.js b/src/database-controller/src/core/config.js index bbe733d210..2596290372 100644 --- a/src/database-controller/src/core/config.js +++ b/src/database-controller/src/core/config.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const Joi = require('joi') diff --git a/src/database-controller/src/core/framework.js b/src/database-controller/src/core/framework.js index 108e4dfa14..61acaaa12c 100644 --- a/src/database-controller/src/core/framework.js +++ b/src/database-controller/src/core/framework.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const logger = require('@dbc/core/logger') const k8s = require('@dbc/core/k8s') diff --git a/src/database-controller/src/core/k8s.js b/src/database-controller/src/core/k8s.js index 7ad48c86cf..e68e0dfbb3 100644 --- a/src/database-controller/src/core/k8s.js +++ b/src/database-controller/src/core/k8s.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const k8s = require('@kubernetes/client-node') const logger = require('./logger') diff --git a/src/database-controller/src/core/logger.js b/src/database-controller/src/core/logger.js index 60289004ce..97c8d3196b 100644 --- a/src/database-controller/src/core/logger.js +++ b/src/database-controller/src/core/logger.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const util = require('util') const winston = require('winston') diff --git a/src/database-controller/src/core/util.js b/src/database-controller/src/core/util.js index d637d45e19..a4166240eb 100644 --- a/src/database-controller/src/core/util.js +++ b/src/database-controller/src/core/util.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const logger = require('@dbc/core/logger') diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index 6ae860f48b..efe13eae1f 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. require('module-alias/register') require('dotenv').config() diff --git a/src/database-controller/src/poller/config.js b/src/database-controller/src/poller/config.js index c95eb1d4eb..ccca9c7882 100644 --- a/src/database-controller/src/poller/config.js +++ b/src/database-controller/src/poller/config.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const basicConfig = require('@dbc/core/config') const _ = require('lodash') diff --git a/src/database-controller/src/poller/index.js b/src/database-controller/src/poller/index.js index 1a3175a28a..da4e6e8598 100644 --- a/src/database-controller/src/poller/index.js +++ b/src/database-controller/src/poller/index.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. require('module-alias/register') require('dotenv').config() diff --git a/src/database-controller/src/watcher/framework/config.js b/src/database-controller/src/watcher/framework/config.js index 4a4098abf0..29aeab9a9e 100644 --- a/src/database-controller/src/watcher/framework/config.js +++ b/src/database-controller/src/watcher/framework/config.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const basicConfig = require('@dbc/core/config') const _ = require('lodash') diff --git a/src/database-controller/src/watcher/framework/index.js b/src/database-controller/src/watcher/framework/index.js index b3346738c7..92d64f4ac1 100644 --- a/src/database-controller/src/watcher/framework/index.js +++ b/src/database-controller/src/watcher/framework/index.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. require('module-alias/register') require('dotenv').config() diff --git a/src/database-controller/src/write-merger/app.js b/src/database-controller/src/write-merger/app.js index dd103bd0ad..696343a6be 100644 --- a/src/database-controller/src/write-merger/app.js +++ b/src/database-controller/src/write-merger/app.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const cors = require('cors') const morgan = require('morgan') diff --git a/src/database-controller/src/write-merger/config.js b/src/database-controller/src/write-merger/config.js index 47e40b3e02..316a6f1192 100644 --- a/src/database-controller/src/write-merger/config.js +++ b/src/database-controller/src/write-merger/config.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const basicConfig = require('@dbc/core/config') const _ = require('lodash') diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js index cd003b4842..c0a5109cd2 100644 --- a/src/database-controller/src/write-merger/handler.js +++ b/src/database-controller/src/write-merger/handler.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. const createError = require('http-errors') const logger = require('@dbc/core/logger') diff --git a/src/database-controller/src/write-merger/index.js b/src/database-controller/src/write-merger/index.js index 7d9dee2814..61bb4717e5 100644 --- a/src/database-controller/src/write-merger/index.js +++ b/src/database-controller/src/write-merger/index.js @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Corporation -// All rights reserved. -// -// MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. require('module-alias/register') require('dotenv').config() From 345d3af77053bbd5d4f0fb61d614715562ee0b20 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 4 Aug 2020 10:42:52 +0800 Subject: [PATCH 19/32] fix lint --- src/database-controller/README.md | 2 +- src/database-controller/sdk/.eslintignore | 7 + src/database-controller/sdk/.eslintrc.js | 36 + src/database-controller/sdk/index.js | 454 +++--- src/database-controller/sdk/package.json | 14 +- .../sdk/prettier.config.js | 8 + src/database-controller/sdk/yarn.lock | 1228 ++++++++++++++++- src/database-controller/src/.eslintignore | 7 + src/database-controller/src/.eslintrc.js | 36 + src/database-controller/src/core/config.js | 62 +- src/database-controller/src/core/framework.js | 345 +++-- src/database-controller/src/core/k8s.js | 228 +-- src/database-controller/src/core/logger.js | 46 +- src/database-controller/src/core/util.js | 74 +- .../src/initializer/index.js | 54 +- src/database-controller/src/package.json | 14 +- src/database-controller/src/poller/config.js | 43 +- src/database-controller/src/poller/index.js | 169 +-- .../src/prettier.config.js | 8 + .../src/watcher/framework/config.js | 28 +- .../src/watcher/framework/index.js | 71 +- .../src/write-merger/app.js | 54 +- .../src/write-merger/config.js | 40 +- .../src/write-merger/handler.js | 152 +- .../src/write-merger/index.js | 10 +- src/database-controller/src/yarn.lock | 1022 +++++++++++++- 26 files changed, 3362 insertions(+), 850 deletions(-) create mode 100644 src/database-controller/sdk/.eslintignore create mode 100644 src/database-controller/sdk/.eslintrc.js create mode 100644 src/database-controller/sdk/prettier.config.js create mode 100644 src/database-controller/src/.eslintignore create mode 100644 src/database-controller/src/.eslintrc.js create mode 100644 src/database-controller/src/prettier.config.js diff --git a/src/database-controller/README.md b/src/database-controller/README.md index e96c522c94..cad091e16b 100644 --- a/src/database-controller/README.md +++ b/src/database-controller/README.md @@ -5,4 +5,4 @@ **Environment:** Node.js 8.17.0, use `yarn install` to install all dependencies under `src/` or `sdk/`. To set environmental variables, create a `.env` file under `src`. -**Lint:** Use `npm install standard --global` to isntall Standard.js globally. Then run `npm run lint` under `src/` or `sdk/`. +**Lint:** Run `npm run lintfix` under `src/` or `sdk/`. diff --git a/src/database-controller/sdk/.eslintignore b/src/database-controller/sdk/.eslintignore new file mode 100644 index 0000000000..9d828a1af8 --- /dev/null +++ b/src/database-controller/sdk/.eslintignore @@ -0,0 +1,7 @@ +# ESLint always ignores files in /node_modules/* and /bower_components/*. +/build/** +/deploy/** +/dist/** +/coverage/** +/docs/** +!.eslintrc.js diff --git a/src/database-controller/sdk/.eslintrc.js b/src/database-controller/sdk/.eslintrc.js new file mode 100644 index 0000000000..703d6ae9c1 --- /dev/null +++ b/src/database-controller/sdk/.eslintrc.js @@ -0,0 +1,36 @@ +module.exports = { + plugins: ['eslint-plugin-prettier'], + env: { + browser: false, + es6: true, + node: true, + jquery: true, + }, + extends: ['standard', 'plugin:prettier/recommended', 'prettier'], + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + cookies: 'readonly', + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + 'prettier/prettier': ['error'], + 'max-len': [ + 'error', + { + code: 120, + ignoreComments: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + }, + ], + }, +}; diff --git a/src/database-controller/sdk/index.js b/src/database-controller/sdk/index.js index 8be0deae2b..0c8392edcd 100644 --- a/src/database-controller/sdk/index.js +++ b/src/database-controller/sdk/index.js @@ -1,227 +1,252 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const { Sequelize, Model } = require('sequelize') +const { Sequelize, Model } = require('sequelize'); class DatabaseModel { - constructor (connectionStr, maxConnection = 10) { - const sequelize = new Sequelize( - connectionStr, - { - pool: { - max: maxConnection, - min: 1 - } - } - ) + constructor(connectionStr, maxConnection = 10) { + const sequelize = new Sequelize(connectionStr, { + pool: { + max: maxConnection, + min: 1, + }, + }); class Framework extends Model {} - Framework.init({ - insertedAt: Sequelize.DATE, - name: { - type: Sequelize.STRING(64), - primaryKey: true - }, - namespace: Sequelize.STRING(64), - jobName: Sequelize.STRING(256), - userName: Sequelize.STRING(256), - jobConfig: Sequelize.TEXT, - executionType: Sequelize.STRING(32), - creationTime: Sequelize.DATE, - virtualCluster: Sequelize.STRING(256), - jobPriority: Sequelize.STRING(256), - totalGpuNumber: Sequelize.INTEGER, - totalTaskNumber: Sequelize.INTEGER, - totalTaskRoleNumber: Sequelize.INTEGER, - logPathInfix: Sequelize.STRING(256), - submissionTime: { - type: Sequelize.DATE, - allowNull: false - }, - dockerSecretDef: Sequelize.TEXT, - configSecretDef: Sequelize.TEXT, - priorityClassDef: Sequelize.TEXT, - retries: Sequelize.INTEGER, - retryDelayTime: Sequelize.INTEGER, - platformRetries: Sequelize.INTEGER, - resourceRetries: Sequelize.INTEGER, - userRetries: Sequelize.INTEGER, - completionTime: Sequelize.DATE, - appExitCode: Sequelize.INTEGER, - subState: Sequelize.STRING(32), - state: Sequelize.STRING(32), - snapshot: Sequelize.TEXT, - requestSynced: { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: false + Framework.init( + { + insertedAt: Sequelize.DATE, + name: { + type: Sequelize.STRING(64), + primaryKey: true, + }, + namespace: Sequelize.STRING(64), + jobName: Sequelize.STRING(256), + userName: Sequelize.STRING(256), + jobConfig: Sequelize.TEXT, + executionType: Sequelize.STRING(32), + creationTime: Sequelize.DATE, + virtualCluster: Sequelize.STRING(256), + jobPriority: Sequelize.STRING(256), + totalGpuNumber: Sequelize.INTEGER, + totalTaskNumber: Sequelize.INTEGER, + totalTaskRoleNumber: Sequelize.INTEGER, + logPathInfix: Sequelize.STRING(256), + submissionTime: { + type: Sequelize.DATE, + allowNull: false, + }, + dockerSecretDef: Sequelize.TEXT, + configSecretDef: Sequelize.TEXT, + priorityClassDef: Sequelize.TEXT, + retries: Sequelize.INTEGER, + retryDelayTime: Sequelize.INTEGER, + platformRetries: Sequelize.INTEGER, + resourceRetries: Sequelize.INTEGER, + userRetries: Sequelize.INTEGER, + completionTime: Sequelize.DATE, + appExitCode: Sequelize.INTEGER, + subState: Sequelize.STRING(32), + state: Sequelize.STRING(32), + snapshot: Sequelize.TEXT, + requestSynced: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + apiServerDeleted: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + archived: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, }, - apiServerDeleted: { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: false + { + sequelize, + indexes: [ + { + unique: false, + fields: ['submissionTime'], + }, + ], + modelName: 'framework', + createdAt: 'insertedAt', }, - archived: { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: false - } - }, { - sequelize, - indexes: [{ - unique: false, - fields: ['submissionTime'] - }], - modelName: 'framework', - createdAt: 'insertedAt' - }) + ); class FrameworkHistory extends Model {} - FrameworkHistory.init({ - insertedAt: Sequelize.DATE, - uid: { - type: Sequelize.STRING(36), - primaryKey: true - }, - frameworkName: { - type: Sequelize.STRING(64), - allowNull: false + FrameworkHistory.init( + { + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true, + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false, + }, + attemptIndex: Sequelize.INTEGER, + historyType: { + type: Sequelize.STRING(16), + allowNull: false, + defaultValue: 'retry', + }, + snapshot: Sequelize.TEXT, }, - attemptIndex: Sequelize.INTEGER, - historyType: { - type: Sequelize.STRING(16), - allowNull: false, - defaultValue: 'retry' + { + sequelize, + modelName: 'framework_history', + createdAt: 'insertedAt', + indexes: [ + { + unique: false, + fields: ['frameworkName'], + }, + ], + freezeTableName: true, }, - snapshot: Sequelize.TEXT - }, { - sequelize, - modelName: 'framework_history', - createdAt: 'insertedAt', - indexes: [{ - unique: false, - fields: ['frameworkName'] - }], - freezeTableName: true - }) + ); class Pod extends Model {} - Pod.init({ - insertedAt: Sequelize.DATE, - uid: { - type: Sequelize.STRING(36), - primaryKey: true + Pod.init( + { + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true, + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false, + }, + attemptIndex: Sequelize.INTEGER, + taskroleName: Sequelize.STRING(256), + taskroleIndex: Sequelize.INTEGER, + taskAttemptIndex: Sequelize.INTEGER, + snapshot: Sequelize.TEXT, }, - frameworkName: { - type: Sequelize.STRING(64), - allowNull: false + { + sequelize, + modelName: 'pod', + createdAt: 'insertedAt', + indexes: [ + { + unique: false, + fields: ['frameworkName'], + }, + ], }, - attemptIndex: Sequelize.INTEGER, - taskroleName: Sequelize.STRING(256), - taskroleIndex: Sequelize.INTEGER, - taskAttemptIndex: Sequelize.INTEGER, - snapshot: Sequelize.TEXT - }, { - sequelize, - modelName: 'pod', - createdAt: 'insertedAt', - indexes: [{ - unique: false, - fields: ['frameworkName'] - }] - }) + ); class FrameworkEvent extends Model {} - FrameworkEvent.init({ - insertedAt: Sequelize.DATE, - uid: { - type: Sequelize.STRING(36), - primaryKey: true - }, - frameworkName: { - type: Sequelize.STRING(64), - allowNull: false + FrameworkEvent.init( + { + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true, + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false, + }, + type: { + type: Sequelize.STRING(32), + allowNull: false, + }, + message: Sequelize.TEXT, + event: Sequelize.TEXT, }, - type: { - type: Sequelize.STRING(32), - allowNull: false + { + sequelize, + modelName: 'framework_event', + createdAt: 'insertedAt', + indexes: [ + { + unique: false, + fields: ['frameworkName'], + }, + ], }, - message: Sequelize.TEXT, - event: Sequelize.TEXT - }, { - sequelize, - modelName: 'framework_event', - createdAt: 'insertedAt', - indexes: [{ - unique: false, - fields: ['frameworkName'] - }] - }) + ); class PodEvent extends Model {} - PodEvent.init({ - insertedAt: Sequelize.DATE, - uid: { - type: Sequelize.STRING(36), - primaryKey: true - }, - frameworkName: { - type: Sequelize.STRING(64), - allowNull: false - }, - podUid: { - type: Sequelize.STRING(36), - allowNull: false + PodEvent.init( + { + insertedAt: Sequelize.DATE, + uid: { + type: Sequelize.STRING(36), + primaryKey: true, + }, + frameworkName: { + type: Sequelize.STRING(64), + allowNull: false, + }, + podUid: { + type: Sequelize.STRING(36), + allowNull: false, + }, + type: { + type: Sequelize.STRING(32), + allowNull: false, + }, + message: Sequelize.TEXT, + event: Sequelize.TEXT, }, - type: { - type: Sequelize.STRING(32), - allowNull: false + { + sequelize, + modelName: 'pod_event', + createdAt: 'insertedAt', + indexes: [ + { + unique: false, + fields: ['frameworkName'], + }, + ], }, - message: Sequelize.TEXT, - event: Sequelize.TEXT - }, { - sequelize, - modelName: 'pod_event', - createdAt: 'insertedAt', - indexes: [{ - unique: false, - fields: ['frameworkName'] - }] - }) + ); - Framework.hasMany(FrameworkHistory) - Framework.hasMany(Pod) - Framework.hasMany(FrameworkEvent) - Framework.hasMany(PodEvent) + Framework.hasMany(FrameworkHistory); + Framework.hasMany(Pod); + Framework.hasMany(FrameworkEvent); + Framework.hasMany(PodEvent); class Version extends Model {} - Version.init({ - version: { - type: Sequelize.STRING(36) + Version.init( + { + version: { + type: Sequelize.STRING(36), + }, + commitVersion: { + type: Sequelize.STRING(64), + }, }, - commitVersion: { - type: Sequelize.STRING(64) - } - }, { - sequelize, - modelName: 'version', - freezeTableName: true - }) + { + sequelize, + modelName: 'version', + freezeTableName: true, + }, + ); // bind to `this` - this.sequelize = sequelize - this.Framework = Framework - this.FrameworkHistory = FrameworkHistory - this.Pod = Pod - this.FrameworkEvent = FrameworkEvent - this.PodEvent = PodEvent - this.Version = Version - this.synchronizeSchema = this.synchronizeSchema.bind(this) + this.sequelize = sequelize; + this.Framework = Framework; + this.FrameworkHistory = FrameworkHistory; + this.Pod = Pod; + this.FrameworkEvent = FrameworkEvent; + this.PodEvent = PodEvent; + this.Version = Version; + this.synchronizeSchema = this.synchronizeSchema.bind(this); } - async synchronizeSchema (force = false) { + async synchronizeSchema(force = false) { if (force === true) { - await this.sequelize.sync({ force: true }) + await this.sequelize.sync({ force: true }); } else { await Promise.all([ this.Framework.sync({ alter: true }), @@ -229,45 +254,48 @@ class DatabaseModel { this.Pod.sync({ alter: true }), this.FrameworkEvent.sync({ alter: true }), this.PodEvent.sync({ alter: true }), - this.Version.sync({ alter: true }) - ]) + this.Version.sync({ alter: true }), + ]); } } - async ping () { - await this.sequelize.authenticate() + async ping() { + await this.sequelize.authenticate(); } - async getVersion () { - let res + async getVersion() { + let res; try { - res = await this.Version.findOne() - } catch (err) { - } + res = await this.Version.findOne(); + } catch (err) {} if (res) { return { version: res.version, - commitVersion: res.commitVersion - } + commitVersion: res.commitVersion, + }; } else { return { version: null, - commitVersion: null - } + commitVersion: null, + }; } } - async setVersion (version, commitVersion) { - await this.sequelize.transaction(async (t) => { + async setVersion(version, commitVersion) { + await this.sequelize.transaction(async t => { await this.Version.destroy({ - where: {}, transaction: t - }) - await this.Version.create({ - version: version, - commitVersion: commitVersion - }, { transaction: t }) - }) + where: {}, + transaction: t, + }); + await this.Version.create( + { + version: version, + commitVersion: commitVersion, + }, + { transaction: t }, + ); + }); } } -module.exports = DatabaseModel +module.exports = DatabaseModel; diff --git a/src/database-controller/sdk/package.json b/src/database-controller/sdk/package.json index 70c84a3138..4af8f555b9 100644 --- a/src/database-controller/sdk/package.json +++ b/src/database-controller/sdk/package.json @@ -3,12 +3,24 @@ "version": "1.0.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "lint": "standard.cmd --fix" + "lint": "eslint --ext .js --ext .jsx .", + "lintfix": "eslint --ext .js --ext .jsx . --fix" }, "main": "index.js", "license": "MIT", "dependencies": { "pg": "^8.2.1", "sequelize": "5.21.3" + }, + "devDependencies": { + "eslint": "6.2.2", + "eslint-config-prettier": "6.1.0", + "eslint-config-standard": "14.0.1", + "eslint-plugin-import": "2.18.2", + "eslint-plugin-node": "9.1.0", + "eslint-plugin-prettier": "3.1.0", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-standard": "4.0.1", + "prettier": "1.18.2" } } diff --git a/src/database-controller/sdk/prettier.config.js b/src/database-controller/sdk/prettier.config.js new file mode 100644 index 0000000000..219e71e448 --- /dev/null +++ b/src/database-controller/sdk/prettier.config.js @@ -0,0 +1,8 @@ +module.exports = { + semi: true, + // Trailing commas help with git merging and conflict resolution + trailingComma: 'all', + // Use single quote in all files. https://github.com/prettier/prettier/issues/1080#issuecomment-390363232 + singleQuote: true, + jsxSingleQuote: true, +}; diff --git a/src/database-controller/sdk/yarn.lock b/src/database-controller/sdk/yarn.lock index dd25fd79d1..7d94375ba6 100644 --- a/src/database-controller/sdk/yarn.lock +++ b/src/database-controller/sdk/yarn.lock @@ -2,26 +2,154 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@types/node@*": version "14.0.14" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + +acorn@^7.1.1: + version "7.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" + integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + +ajv@^6.10.0, ajv@^6.10.2: + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + any-promise@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-includes@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" + integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + is-string "^1.0.5" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + bluebird@^3.5.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + buffer-writer@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" + integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== + cls-bluebird@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cls-bluebird/-/cls-bluebird-2.1.0.tgz#37ef1e080a8ffb55c2f4164f536f1919e7968aee" @@ -30,33 +158,667 @@ cls-bluebird@^2.1.0: is-bluebird "^1.0.2" shimmer "^1.1.0" -debug@^4.1.1: +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.0.1, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dottie@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.2.tgz#cc91c0726ce3a054ebf11c55fbc92a7f266dd154" integrity sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg== +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-regex "^1.1.0" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-config-prettier@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.1.0.tgz#e6f678ba367fbd1273998d5510f76f004e9dce7b" + integrity sha512-k9fny9sPjIBQ2ftFTesJV21Rg4R/7a7t7LCtZVrYQiHEp8Nnuk3EGaDmsKSAnsPj0BYcgB2zxzHa2NTkIxcOLg== + dependencies: + get-stdin "^6.0.0" + +eslint-config-standard@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.0.1.tgz#375c3636fb4bd453cb95321d873de12e4eef790b" + integrity sha512-1RWsAKTDTZgA8bIM6PSC9aTGDAUlKqNkYNJlTZ5xYD/HYkIM6GlcefFvgcJ8xi0SWG5203rttKYX28zW+rKNOg== + +eslint-import-resolver-node@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-module-utils@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + +eslint-plugin-es@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998" + integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA== + dependencies: + eslint-utils "^1.4.2" + regexpp "^2.0.1" + +eslint-plugin-import@2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" + integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== + dependencies: + array-includes "^3.0.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.4.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.0" + read-pkg-up "^2.0.0" + resolve "^1.11.0" + +eslint-plugin-node@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a" + integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw== + dependencies: + eslint-plugin-es "^1.4.0" + eslint-utils "^1.3.1" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-prettier@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz#8695188f95daa93b0dc54b249347ca3b79c4686d" + integrity sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-plugin-promise@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" + integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== + +eslint-plugin-standard@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" + integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== + +eslint-scope@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" + integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.3.1, eslint-utils@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint@6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f" + integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.2" + eslint-visitor-keys "^1.1.0" + espree "^6.1.1" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^11.7.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^6.4.1" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + +glob-parent@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.7.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.1: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + inflection@1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@^6.4.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" + integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + is-bluebird@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2" integrity sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI= +is-callable@^1.1.4, is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" + integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== + dependencies: + has-symbols "^1.0.1" + +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +isarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash@^4.17.12, lodash@^4.17.14: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + moment-timezone@^0.5.21: version "0.5.31" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" @@ -69,16 +831,167 @@ moment-timezone@^0.5.21: resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +object-inspect@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.values@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + packet-reader@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + pg-connection-string@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.2.3.tgz#48e1158ec37eaa82e98dbcb7307103ec303fe0e7" @@ -131,6 +1044,18 @@ pgpass@1.x: dependencies: split "^1.0.0" +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -153,6 +1078,75 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.13.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + retry-as-promised@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543" @@ -160,12 +1154,41 @@ retry-as-promised@^3.2.0: dependencies: any-promise "^1.3.0" +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-async@^2.2.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +rxjs@^6.4.0: + version "6.6.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== + dependencies: + tslib "^1.9.0" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= -semver@^6.3.0: +semver@^6.1.0, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -196,11 +1219,63 @@ sequelize@5.21.3: validator "^10.11.0" wkx "^0.4.8" +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + shimmer@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== +signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -208,26 +1283,156 @@ split@^1.0.0: dependencies: through "2" -through@2: +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-json-comments@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through@2, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + toposort-class@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= +tslib@^1.9.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +v8-compile-cache@^2.0.3: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + validator@^10.11.0: version "10.11.0" resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + wkx@^0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.8.tgz#a092cf088d112683fdc7182fd31493b2c5820003" @@ -235,6 +1440,23 @@ wkx@^0.4.8: dependencies: "@types/node" "*" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" diff --git a/src/database-controller/src/.eslintignore b/src/database-controller/src/.eslintignore new file mode 100644 index 0000000000..9d828a1af8 --- /dev/null +++ b/src/database-controller/src/.eslintignore @@ -0,0 +1,7 @@ +# ESLint always ignores files in /node_modules/* and /bower_components/*. +/build/** +/deploy/** +/dist/** +/coverage/** +/docs/** +!.eslintrc.js diff --git a/src/database-controller/src/.eslintrc.js b/src/database-controller/src/.eslintrc.js new file mode 100644 index 0000000000..703d6ae9c1 --- /dev/null +++ b/src/database-controller/src/.eslintrc.js @@ -0,0 +1,36 @@ +module.exports = { + plugins: ['eslint-plugin-prettier'], + env: { + browser: false, + es6: true, + node: true, + jquery: true, + }, + extends: ['standard', 'plugin:prettier/recommended', 'prettier'], + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + cookies: 'readonly', + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + 'prettier/prettier': ['error'], + 'max-len': [ + 'error', + { + code: 120, + ignoreComments: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + }, + ], + }, +}; diff --git a/src/database-controller/src/core/config.js b/src/database-controller/src/core/config.js index 2596290372..1d432445c3 100644 --- a/src/database-controller/src/core/config.js +++ b/src/database-controller/src/core/config.js @@ -1,45 +1,47 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const Joi = require('joi') +const Joi = require('joi'); -const configSchema = Joi.object().keys({ - logLevel: Joi.string() - .valid('error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly') - .default('info'), - k8sConnectionTimeoutSecond: Joi.number() - .integer() - .required(), - writeMergerConnectionTimeoutSecond: Joi.number() - .integer() - .required(), - customK8sApiServerURL: Joi.string() - .uri() - .optional(), - customK8sCaFile: Joi.string() - .optional(), - customK8sTokenFile: Joi.string() - .optional(), - recoveryModeEnabled: Joi.boolean() - .required(), - rbacEnabled: Joi.boolean() - .required() -}).required() +const configSchema = Joi.object() + .keys({ + logLevel: Joi.string() + .valid('error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly') + .default('info'), + k8sConnectionTimeoutSecond: Joi.number() + .integer() + .required(), + writeMergerConnectionTimeoutSecond: Joi.number() + .integer() + .required(), + customK8sApiServerURL: Joi.string() + .uri() + .optional(), + customK8sCaFile: Joi.string().optional(), + customK8sTokenFile: Joi.string().optional(), + recoveryModeEnabled: Joi.boolean().required(), + rbacEnabled: Joi.boolean().required(), + }) + .required(); const config = { logLevel: process.env.LOG_LEVEL, - k8sConnectionTimeoutSecond: parseInt(process.env.K8S_CONNECTION_TIMEOUT_SECOND), - writeMergerConnectionTimeoutSecond: parseInt(process.env.WRITE_MERGER_CONNECTION_TIMEOUT_SECOND), + k8sConnectionTimeoutSecond: parseInt( + process.env.K8S_CONNECTION_TIMEOUT_SECOND, + ), + writeMergerConnectionTimeoutSecond: parseInt( + process.env.WRITE_MERGER_CONNECTION_TIMEOUT_SECOND, + ), customK8sApiServerURL: process.env.CUSTOM_K8S_API_SERVER_URL, customK8sCaFile: process.env.CUSTOM_K8S_CA_FILE, customK8sTokenFile: process.env.CUSTOM_K8S_TOKEN_FILE, recoveryModeEnabled: process.env.RECOVERY_MODE_ENABLED === 'true', - rbacEnabled: process.env.RBAC_IN_CLUSTER === 'true' -} + rbacEnabled: process.env.RBAC_IN_CLUSTER === 'true', +}; -const { error, value } = Joi.validate(config, configSchema) +const { error, value } = Joi.validate(config, configSchema); if (error) { - throw new Error(`Config error\n${error}`) + throw new Error(`Config error\n${error}`); } -module.exports = value +module.exports = value; diff --git a/src/database-controller/src/core/framework.js b/src/database-controller/src/core/framework.js index 61acaaa12c..9ae8f8baa9 100644 --- a/src/database-controller/src/core/framework.js +++ b/src/database-controller/src/core/framework.js @@ -1,63 +1,63 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const logger = require('@dbc/core/logger') -const k8s = require('@dbc/core/k8s') -const _ = require('lodash') -const yaml = require('js-yaml') +const logger = require('@dbc/core/logger'); +const k8s = require('@dbc/core/k8s'); +const _ = require('lodash'); +const yaml = require('js-yaml'); const mockFrameworkStatus = () => { return { state: 'AttemptCreationPending', attemptStatus: { completionStatus: null, - taskRoleStatuses: [] + taskRoleStatuses: [], }, retryPolicyStatus: { retryDelaySec: null, totalRetriedCount: 0, - accountableRetriedCount: 0 - } - } -} + accountableRetriedCount: 0, + }, + }; +}; const convertState = (state, exitCode, retryDelaySec) => { switch (state) { case 'AttemptCreationPending': case 'AttemptCreationRequested': case 'AttemptPreparing': - return 'WAITING' + return 'WAITING'; case 'AttemptRunning': - return 'RUNNING' + return 'RUNNING'; case 'AttemptDeletionPending': case 'AttemptDeletionRequested': case 'AttemptDeleting': if (exitCode === -210 || exitCode === -220) { - return 'STOPPING' + return 'STOPPING'; } else { - return 'RUNNING' + return 'RUNNING'; } case 'AttemptCompleted': if (retryDelaySec == null) { - return 'RUNNING' + return 'RUNNING'; } else { - return 'WAITING' + return 'WAITING'; } case 'Completed': if (exitCode === 0) { - return 'SUCCEEDED' + return 'SUCCEEDED'; } else if (exitCode === -210 || exitCode === -220) { - return 'STOPPED' + return 'STOPPED'; } else { - return 'FAILED' + return 'FAILED'; } default: - return 'UNKNOWN' + return 'UNKNOWN'; } -} +}; -function ignoreError (err) { - logger.info('This error will be ignored: ', err) +function ignoreError(err) { + logger.info('This error will be ignored: ', err); } // Class `Snapshot` handles the full json of framework. @@ -67,56 +67,64 @@ function ignoreError (err) { // getRequestUpdate, getStatusUpdate, getAllUpdate: Get database updates from the snapshot. // It doesn't handle database internal status, like: requestSynced, apiServerDeleted, ..., etc. class Snapshot { - constructor (snapshot) { + constructor(snapshot) { if (snapshot instanceof Object) { - this._snapshot = _.cloneDeep(snapshot) + this._snapshot = _.cloneDeep(snapshot); } else { - this._snapshot = JSON.parse(snapshot) + this._snapshot = JSON.parse(snapshot); } // If the snapshot doesn't have a status, mock one instead. // This usually happens when the framework spec is generated by rest-server. if (!this._snapshot.status) { - this._snapshot.status = mockFrameworkStatus() + this._snapshot.status = mockFrameworkStatus(); } } - copy () { - return new Snapshot(this._snapshot) + copy() { + return new Snapshot(this._snapshot); } - getRequest (omitGeneration) { + getRequest(omitGeneration) { const request = _.pick(this._snapshot, [ 'apiVersion', 'kind', 'metadata.name', 'metadata.labels', 'metadata.annotations', - 'spec' - ]) + 'spec', + ]); if (omitGeneration) { - return _.omit(request, 'metadata.annotations.requestGeneration') + return _.omit(request, 'metadata.annotations.requestGeneration'); } else { - return request + return request; } } - overrideRequest (otherSnapshot) { + overrideRequest(otherSnapshot) { // shouldn't use _.merge here - _.assign(this._snapshot, _.pick(otherSnapshot._snapshot, [ - 'apiVersion', - 'kind', - 'spec' - ])) - _.assign(this._snapshot.metadata, _.pick(otherSnapshot._snapshot.metadata, [ - 'name', - 'labels', - 'annotations' - ])) + _.assign( + this._snapshot, + _.pick(otherSnapshot._snapshot, ['apiVersion', 'kind', 'spec']), + ); + _.assign( + this._snapshot.metadata, + _.pick(otherSnapshot._snapshot.metadata, [ + 'name', + 'labels', + 'annotations', + ]), + ); } - getRequestUpdate (withSnapshot = true) { - const loadedConfig = yaml.safeLoad(this._snapshot.metadata.annotations.config) - const jobPriority = _.get(loadedConfig, 'extras.hivedscheduler.jobPriorityClass', null) + getRequestUpdate(withSnapshot = true) { + const loadedConfig = yaml.safeLoad( + this._snapshot.metadata.annotations.config, + ); + const jobPriority = _.get( + loadedConfig, + 'extras.hivedscheduler.jobPriorityClass', + null, + ); const update = { name: this._snapshot.metadata.name, namespace: this._snapshot.metadata.namespace, @@ -127,244 +135,287 @@ class Snapshot { virtualCluster: this._snapshot.metadata.labels.virtualCluster, jobPriority: jobPriority, totalGpuNumber: this._snapshot.metadata.annotations.totalGpuNumber, - totalTaskNumber: this._snapshot.spec.taskRoles.reduce((num, spec) => num + spec.taskNumber, 0), + totalTaskNumber: this._snapshot.spec.taskRoles.reduce( + (num, spec) => num + spec.taskNumber, + 0, + ), totalTaskRoleNumber: this._snapshot.spec.taskRoles.length, - logPathInfix: this._snapshot.metadata.annotations.logPathInfix - } + logPathInfix: this._snapshot.metadata.annotations.logPathInfix, + }; if (withSnapshot) { - update.snapshot = JSON.stringify(this._snapshot) + update.snapshot = JSON.stringify(this._snapshot); } - return update + return update; } - getStatusUpdate (withSnapshot = true) { - const completionStatus = this._snapshot.status.attemptStatus.completionStatus + getStatusUpdate(withSnapshot = true) { + const completionStatus = this._snapshot.status.attemptStatus + .completionStatus; const update = { retries: this._snapshot.status.retryPolicyStatus.totalRetriedCount, retryDelayTime: this._snapshot.status.retryPolicyStatus.retryDelaySec, - platformRetries: this._snapshot.status.retryPolicyStatus.totalRetriedCount - this._snapshot.status.retryPolicyStatus.accountableRetriedCount, + platformRetries: + this._snapshot.status.retryPolicyStatus.totalRetriedCount - + this._snapshot.status.retryPolicyStatus.accountableRetriedCount, resourceRetries: 0, - userRetries: this._snapshot.status.retryPolicyStatus.accountableRetriedCount, - creationTime: this._snapshot.metadata.creationTimestamp ? new Date(this._snapshot.metadata.creationTimestamp) : null, - completionTime: this._snapshot.status.completionTime ? new Date(this._snapshot.status.completionTime) : null, + userRetries: this._snapshot.status.retryPolicyStatus + .accountableRetriedCount, + creationTime: this._snapshot.metadata.creationTimestamp + ? new Date(this._snapshot.metadata.creationTimestamp) + : null, + completionTime: this._snapshot.status.completionTime + ? new Date(this._snapshot.status.completionTime) + : null, appExitCode: completionStatus ? completionStatus.code : null, subState: this._snapshot.status.state, state: convertState( this._snapshot.status.state, completionStatus ? completionStatus.code : null, - this._snapshot.status.retryPolicyStatus.retryDelaySec - ) - } + this._snapshot.status.retryPolicyStatus.retryDelaySec, + ), + }; if (withSnapshot) { - update.snapshot = JSON.stringify(this._snapshot) + update.snapshot = JSON.stringify(this._snapshot); } - return update + return update; } - getAllUpdate (withSnapshot = true) { - const update = _.assign({}, this.getRequestUpdate(false), this.getStatusUpdate(false)) + getAllUpdate(withSnapshot = true) { + const update = _.assign( + {}, + this.getRequestUpdate(false), + this.getStatusUpdate(false), + ); if (withSnapshot) { - update.snapshot = JSON.stringify(this._snapshot) + update.snapshot = JSON.stringify(this._snapshot); } - return update + return update; } - getRecordForLegacyTransfer () { - const record = this.getAllUpdate() + getRecordForLegacyTransfer() { + const record = this.getAllUpdate(); // correct submissionTime is lost, use snapshot.metadata.creationTimestamp instead if (this.hasCreationTime()) { - record.submissionTime = this.getCreationTime() + record.submissionTime = this.getCreationTime(); } else { - record.submissionTime = new Date() + record.submissionTime = new Date(); } - this.setGeneration(1) - return record + this.setGeneration(1); + return record; } - getName () { - return this._snapshot.metadata.name + getName() { + return this._snapshot.metadata.name; } - getSnapshot () { - return _.cloneDeep(this._snapshot) + getSnapshot() { + return _.cloneDeep(this._snapshot); } - getString () { - return JSON.stringify(this._snapshot) + getString() { + return JSON.stringify(this._snapshot); } - hasCreationTime () { + hasCreationTime() { if (_.get(this._snapshot, 'metadata.creationTimestamp')) { - return true + return true; } else { - return false + return false; } } - getCreationTime () { + getCreationTime() { if (this.hasCreationTime()) { - return new Date(this._snapshot.metadata.creationTimestamp) + return new Date(this._snapshot.metadata.creationTimestamp); } else { - return null + return null; } } - setGeneration (generation) { - this._snapshot.metadata.annotations.requestGeneration = (generation).toString() + setGeneration(generation) { + this._snapshot.metadata.annotations.requestGeneration = generation.toString(); } - getGeneration () { + getGeneration() { if (!_.has(this._snapshot, 'metadata.annotations.requestGeneration')) { // for some legacy jobs, use 1 as its request generation. - this.setGeneration(1) + this.setGeneration(1); } - return parseInt(this._snapshot.metadata.annotations.requestGeneration) + return parseInt(this._snapshot.metadata.annotations.requestGeneration); } } // Class Add-ons handles creation/patching/deletion of job add-ons. // Currently there are 3 types of add-ons: configSecret, priorityClass, and dockerSecret. class AddOns { - constructor (configSecretDef = null, priorityClassDef = null, dockerSecretDef = null) { + constructor( + configSecretDef = null, + priorityClassDef = null, + dockerSecretDef = null, + ) { if (configSecretDef !== null && !(configSecretDef instanceof Object)) { - this._configSecretDef = JSON.parse(configSecretDef) + this._configSecretDef = JSON.parse(configSecretDef); } else { - this._configSecretDef = configSecretDef + this._configSecretDef = configSecretDef; } if (priorityClassDef !== null && !(priorityClassDef instanceof Object)) { - this._priorityClassDef = JSON.parse(priorityClassDef) + this._priorityClassDef = JSON.parse(priorityClassDef); } else { - this._priorityClassDef = priorityClassDef + this._priorityClassDef = priorityClassDef; } if (dockerSecretDef !== null && !(dockerSecretDef instanceof Object)) { - this._dockerSecretDef = JSON.parse(dockerSecretDef) + this._dockerSecretDef = JSON.parse(dockerSecretDef); } else { - this._dockerSecretDef = dockerSecretDef + this._dockerSecretDef = dockerSecretDef; } } - async create () { + async create() { if (this._configSecretDef) { try { - await k8s.createSecret(this._configSecretDef) + await k8s.createSecret(this._configSecretDef); } catch (err) { if (err.response && err.response.statusCode === 409) { - logger.warn(`Secret ${this._configSecretDef.metadata.name} already exists.`) + logger.warn( + `Secret ${this._configSecretDef.metadata.name} already exists.`, + ); } else { - throw err + throw err; } } } if (this._priorityClassDef) { try { - await k8s.createPriorityClass(this._priorityClassDef) + await k8s.createPriorityClass(this._priorityClassDef); } catch (err) { if (err.response && err.response.statusCode === 409) { - logger.warn(`PriorityClass ${this._priorityClassDef.metadata.name} already exists.`) + logger.warn( + `PriorityClass ${this._priorityClassDef.metadata.name} already exists.`, + ); } else { - throw err + throw err; } } } if (this._dockerSecretDef) { try { - await k8s.createSecret(this._dockerSecretDef) + await k8s.createSecret(this._dockerSecretDef); } catch (err) { if (err.response && err.response.statusCode === 409) { - logger.warn(`Secret ${this._dockerSecretDef.metadata.name} already exists.`) + logger.warn( + `Secret ${this._dockerSecretDef.metadata.name} already exists.`, + ); } else { - throw err + throw err; } } } } - silentPatch (frameworkResponse) { + silentPatch(frameworkResponse) { // do not await for patch - this._configSecretDef && k8s.patchSecretOwnerToFramework(this._configSecretDef, frameworkResponse).catch(ignoreError) - this._dockerSecretDef && k8s.patchSecretOwnerToFramework(this._dockerSecretDef, frameworkResponse).catch(ignoreError) + this._configSecretDef && + k8s + .patchSecretOwnerToFramework(this._configSecretDef, frameworkResponse) + .catch(ignoreError); + this._dockerSecretDef && + k8s + .patchSecretOwnerToFramework(this._dockerSecretDef, frameworkResponse) + .catch(ignoreError); } - silentDelete () { + silentDelete() { // do not await for delete - this._configSecretDef && k8s.deleteSecret(this._configSecretDef.metadata.name).catch(ignoreError) - this._priorityClassDef && k8s.deletePriorityClass(this._priorityClassDef.metadata.name).catch(ignoreError) - this._dockerSecretDef && k8s.deleteSecret(this._dockerSecretDef.metadata.name).catch(ignoreError) + this._configSecretDef && + k8s.deleteSecret(this._configSecretDef.metadata.name).catch(ignoreError); + this._priorityClassDef && + k8s + .deletePriorityClass(this._priorityClassDef.metadata.name) + .catch(ignoreError); + this._dockerSecretDef && + k8s.deleteSecret(this._dockerSecretDef.metadata.name).catch(ignoreError); } - getUpdate () { - const update = {} + getUpdate() { + const update = {}; if (this._configSecretDef) { - update.configSecretDef = JSON.stringify(this._configSecretDef) + update.configSecretDef = JSON.stringify(this._configSecretDef); } if (this._priorityClassDef) { - update.priorityClassDef = JSON.stringify(this._priorityClassDef) + update.priorityClassDef = JSON.stringify(this._priorityClassDef); } if (this._dockerSecretDef) { - update.dockerSecretDef = JSON.stringify(this._dockerSecretDef) + update.dockerSecretDef = JSON.stringify(this._dockerSecretDef); } - return update + return update; } } -async function synchronizeCreate (snapshot, addOns) { - await addOns.create() +async function synchronizeCreate(snapshot, addOns) { + await addOns.create(); try { - const response = await k8s.createFramework(snapshot.getRequest(false)) + const response = await k8s.createFramework(snapshot.getRequest(false)); // framework is created successfully. - const frameworkResponse = response.body + const frameworkResponse = response.body; // don't wait for patching - addOns.silentPatch(frameworkResponse) - return frameworkResponse + addOns.silentPatch(frameworkResponse); + return frameworkResponse; } catch (err) { if (err.response && err.response.statusCode === 409) { // doesn't delete add-ons if 409 error - logger.warn(`Framework ${snapshot.getName()} already exists.`) - throw err + logger.warn(`Framework ${snapshot.getName()} already exists.`); + throw err; } else { // delete add-ons if 409 error - addOns.silentDelete() - throw err + addOns.silentDelete(); + throw err; } } } -async function synchronizeModify (snapshot) { - const response = await k8s.patchFramework(snapshot.getName(), snapshot.getRequest(false)) - const frameworkResponse = response.body - return frameworkResponse +async function synchronizeModify(snapshot) { + const response = await k8s.patchFramework( + snapshot.getName(), + snapshot.getRequest(false), + ); + const frameworkResponse = response.body; + return frameworkResponse; } -async function synchronizeRequest (snapshot, addOns) { +async function synchronizeRequest(snapshot, addOns) { // any error will be raised // if succeed, return framework from api server // There may be multiple calls of synchronizeRequest. // Poller and write-merger uses this method. try { - await k8s.getFramework(snapshot.getName()) + await k8s.getFramework(snapshot.getName()); // if framework exists - const frameworkResponse = await synchronizeModify(snapshot) - logger.info(`Request of framework ${snapshot.getName()} is successfully patched.`) - return frameworkResponse + const frameworkResponse = await synchronizeModify(snapshot); + logger.info( + `Request of framework ${snapshot.getName()} is successfully patched.`, + ); + return frameworkResponse; } catch (err) { if (err.response && err.response.statusCode === 404) { - const frameworkResponse = await synchronizeCreate(snapshot, addOns) - logger.info(`Request of framework ${snapshot.getName()} is successfully created.`) - return frameworkResponse + const frameworkResponse = await synchronizeCreate(snapshot, addOns); + logger.info( + `Request of framework ${snapshot.getName()} is successfully created.`, + ); + return frameworkResponse; } else { - throw err + throw err; } } } -function silentSynchronizeRequest (snapshot, addOns) { +function silentSynchronizeRequest(snapshot, addOns) { // any error will be ignored - synchronizeRequest(snapshot, addOns).catch(ignoreError) + synchronizeRequest(snapshot, addOns).catch(ignoreError); } module.exports = { Snapshot, AddOns, synchronizeRequest, - silentSynchronizeRequest -} + silentSynchronizeRequest, +}; diff --git a/src/database-controller/src/core/k8s.js b/src/database-controller/src/core/k8s.js index e68e0dfbb3..3a6621dd8f 100644 --- a/src/database-controller/src/core/k8s.js +++ b/src/database-controller/src/core/k8s.js @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const k8s = require('@kubernetes/client-node') -const logger = require('./logger') -const config = require('./config') -const { timeoutDecorator } = require('./util') -const kc = new k8s.KubeConfig() +const k8s = require('@kubernetes/client-node'); +const logger = require('./logger'); +const config = require('./config'); +const { timeoutDecorator } = require('./util'); +const kc = new k8s.KubeConfig(); if (config.rbacEnabled) { // If RBAC is enabled, we can use kc.loadFromDefault() to load k8s config in containers. @@ -16,28 +16,26 @@ if (config.rbacEnabled) { name: 'inCluster', caFile: config.customK8sCaFile, server: config.customK8sApiServerURL, - skipTLSVerify: false - } + skipTLSVerify: false, + }; const user = { name: 'inClusterUser', - authProvider: - { - name: 'tokenFile', - config: - { - tokenFile: config.customK8sTokenFile - } - } - } - kc.loadFromClusterAndUser(cluster, user) + authProvider: { + name: 'tokenFile', + config: { + tokenFile: config.customK8sTokenFile, + }, + }, + }; + kc.loadFromClusterAndUser(cluster, user); } else { - kc.loadFromDefault() + kc.loadFromDefault(); } } else { // If RBAC is not enabled, use CUSTOM_K8S_API_SERVER_URL to connect to API server. - const cluster = { name: 'cluster', server: config.customK8sApiServerURL } - const user = { name: 'user' } - kc.loadFromClusterAndUser(cluster, user) + const cluster = { name: 'cluster', server: config.customK8sApiServerURL }; + const user = { name: 'user' }; + kc.loadFromClusterAndUser(cluster, user); } // If API server reports a non-200 status code, the client will @@ -45,41 +43,41 @@ if (config.rbacEnabled) { // and error.response.statusCode is the actual status code. // If network is disconnected, the error will be a different one, // and you cannot get error.name and error.statusCode. -const customObjectsClient = kc.makeApiClient(k8s.CustomObjectsApi) +const customObjectsClient = kc.makeApiClient(k8s.CustomObjectsApi); -async function getFramework (name, namespace = 'default') { +async function getFramework(name, namespace = 'default') { const res = await customObjectsClient.getNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', namespace, 'frameworks', - name - ) - return res.response + name, + ); + return res.response; } -async function listFramework (name, namespace = 'default') { +async function listFramework(name, namespace = 'default') { const res = await customObjectsClient.listNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', namespace, - 'frameworks' - ) - return res.response + 'frameworks', + ); + return res.response; } -async function createFramework (frameworkDescription, namespace = 'default') { +async function createFramework(frameworkDescription, namespace = 'default') { const res = await customObjectsClient.createNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', namespace, 'frameworks', - frameworkDescription - ) - return res.response + frameworkDescription, + ); + return res.response; } -async function patchFramework (name, data, namespace = 'default') { +async function patchFramework(name, data, namespace = 'default') { const res = await customObjectsClient.patchNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', @@ -87,23 +85,26 @@ async function patchFramework (name, data, namespace = 'default') { 'frameworks', name, data, - { headers: { 'Content-Type': 'application/merge-patch+json' } } - ) - return res.response + { headers: { 'Content-Type': 'application/merge-patch+json' } }, + ); + return res.response; } -async function deleteFramework (name, namespace = 'default') { +async function deleteFramework(name, namespace = 'default') { const res = await customObjectsClient.deleteNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', namespace, 'frameworks', - name - ) - return res.response + name, + ); + return res.response; } -function getFrameworkInformer (timeoutSeconds = 365 * 86400, namespace = 'default') { +function getFrameworkInformer( + timeoutSeconds = 365 * 86400, + namespace = 'default', +) { /* Usage: @@ -125,78 +126,125 @@ function getFrameworkInformer (timeoutSeconds = 365 * 86400, namespace = 'defaul */ const listFn = () => { - logger.info('Frameworks are listed.') + logger.info('Frameworks are listed.'); return customObjectsClient.listNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', namespace, - 'frameworks' - ) - } + 'frameworks', + ); + }; const informer = k8s.makeInformer( kc, `/apis/frameworkcontroller.microsoft.com/v1/frameworks?timeoutSeconds=${timeoutSeconds}`, - listFn - ) - return informer + listFn, + ); + return informer; } -const priorityClassClient = kc.makeApiClient(k8s.SchedulingV1Api) +const priorityClassClient = kc.makeApiClient(k8s.SchedulingV1Api); -async function createPriorityClass (priorityClassDef) { - const res = await priorityClassClient.createPriorityClass(priorityClassDef) - return res.response +async function createPriorityClass(priorityClassDef) { + const res = await priorityClassClient.createPriorityClass(priorityClassDef); + return res.response; } -async function deletePriorityClass (name) { - const res = await priorityClassClient.deletePriorityClass(name) - return res.response +async function deletePriorityClass(name) { + const res = await priorityClassClient.deletePriorityClass(name); + return res.response; } -const coreV1Client = kc.makeApiClient(k8s.CoreV1Api) +const coreV1Client = kc.makeApiClient(k8s.CoreV1Api); -async function createSecret (secretDef) { - const res = await coreV1Client.createNamespacedSecret(secretDef.metadata.namespace, secretDef) - return res +async function createSecret(secretDef) { + const res = await coreV1Client.createNamespacedSecret( + secretDef.metadata.namespace, + secretDef, + ); + return res; } -async function deleteSecret (name, namespace = 'default') { - const res = await coreV1Client.deleteNamespacedSecret(name, namespace) - return res.response +async function deleteSecret(name, namespace = 'default') { + const res = await coreV1Client.deleteNamespacedSecret(name, namespace); + return res.response; } -async function patchSecretOwnerToFramework (secret, frameworkResponse) { +async function patchSecretOwnerToFramework(secret, frameworkResponse) { const metadata = { - ownerReferences: [{ - apiVersion: 'frameworkcontroller.microsoft.com', - kind: 'Framework', - name: frameworkResponse.metadata.name, - uid: frameworkResponse.metadata.uid, - controller: true, - blockOwnerDeletion: true - }] - } + ownerReferences: [ + { + apiVersion: 'frameworkcontroller.microsoft.com', + kind: 'Framework', + name: frameworkResponse.metadata.name, + uid: frameworkResponse.metadata.uid, + controller: true, + blockOwnerDeletion: true, + }, + ], + }; const res = await coreV1Client.patchNamespacedSecret( - secret.metadata.name, secret.metadata.namespace, + secret.metadata.name, + secret.metadata.namespace, { metadata: metadata }, ...Array(4), // skip some parameters - { headers: { 'Content-Type': 'application/merge-patch+json' } }) - return res.response + { headers: { 'Content-Type': 'application/merge-patch+json' } }, + ); + return res.response; } -const timeoutMs = config.k8sConnectionTimeoutSecond * 1000 +const timeoutMs = config.k8sConnectionTimeoutSecond * 1000; // We give every method a timeout. module.exports = { - getFramework: timeoutDecorator(getFramework, 'Kubernetes getFramework', timeoutMs), - listFramework: timeoutDecorator(listFramework, 'Kubernetes getFramework', timeoutMs), - createFramework: timeoutDecorator(createFramework, 'Kubernetes createFramework', timeoutMs), - patchFramework: timeoutDecorator(patchFramework, 'Kubernetes patchFramework', timeoutMs), - deleteFramework: timeoutDecorator(deleteFramework, 'Kubernetes deleteFramework', timeoutMs), - createPriorityClass: timeoutDecorator(createPriorityClass, 'Kubernetes createPriorityClass', timeoutMs), - deletePriorityClass: timeoutDecorator(deletePriorityClass, 'Kubernetes deletePriorityClass', timeoutMs), - createSecret: timeoutDecorator(createSecret, 'Kubernetes createSecret', timeoutMs), - deleteSecret: timeoutDecorator(deleteSecret, 'Kubernetes deleteSecret', timeoutMs), - patchSecretOwnerToFramework: timeoutDecorator(patchSecretOwnerToFramework, 'Kubernetes patchSecretOwnerToFramework', timeoutMs), - getFrameworkInformer: getFrameworkInformer -} + getFramework: timeoutDecorator( + getFramework, + 'Kubernetes getFramework', + timeoutMs, + ), + listFramework: timeoutDecorator( + listFramework, + 'Kubernetes getFramework', + timeoutMs, + ), + createFramework: timeoutDecorator( + createFramework, + 'Kubernetes createFramework', + timeoutMs, + ), + patchFramework: timeoutDecorator( + patchFramework, + 'Kubernetes patchFramework', + timeoutMs, + ), + deleteFramework: timeoutDecorator( + deleteFramework, + 'Kubernetes deleteFramework', + timeoutMs, + ), + createPriorityClass: timeoutDecorator( + createPriorityClass, + 'Kubernetes createPriorityClass', + timeoutMs, + ), + deletePriorityClass: timeoutDecorator( + deletePriorityClass, + 'Kubernetes deletePriorityClass', + timeoutMs, + ), + createSecret: timeoutDecorator( + createSecret, + 'Kubernetes createSecret', + timeoutMs, + ), + deleteSecret: timeoutDecorator( + deleteSecret, + 'Kubernetes deleteSecret', + timeoutMs, + ), + patchSecretOwnerToFramework: timeoutDecorator( + patchSecretOwnerToFramework, + 'Kubernetes patchSecretOwnerToFramework', + timeoutMs, + ), + getFrameworkInformer: getFrameworkInformer, +}; diff --git a/src/database-controller/src/core/logger.js b/src/database-controller/src/core/logger.js index 97c8d3196b..71b29e6e01 100644 --- a/src/database-controller/src/core/logger.js +++ b/src/database-controller/src/core/logger.js @@ -1,43 +1,43 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const util = require('util') -const winston = require('winston') -const config = require('./config') +const util = require('util'); +const winston = require('winston'); +const config = require('./config'); const logTransports = { console: new winston.transports.Console({ json: false, colorize: true, timestamp: () => new Date().toISOString(), - formatter: (options) => { - const timestamp = options.timestamp() + formatter: options => { + const timestamp = options.timestamp(); const level = winston.config.colorize( options.level, - options.level.toUpperCase() - ) - const message = options.message ? options.message : '' - const meta = options.meta && Object.keys(options.meta).length - ? '\nmeta = ' + JSON.stringify(options.meta, null, 2) : '' - return util.format(timestamp, '[' + level + ']', message, meta) - } - }) -} + options.level.toUpperCase(), + ); + const message = options.message ? options.message : ''; + const meta = + options.meta && Object.keys(options.meta).length + ? '\nmeta = ' + JSON.stringify(options.meta, null, 2) + : ''; + return util.format(timestamp, '[' + level + ']', message, meta); + }, + }), +}; // create logger const logger = new winston.Logger({ level: config.logLevel, - transports: [ - logTransports.console - ], - exitOnError: false -}) + transports: [logTransports.console], + exitOnError: false, +}); logger.stream = { write: (message, encoding) => { - logger.info(message.trim()) - } -} + logger.info(message.trim()); + }, +}; // module exports -module.exports = logger +module.exports = logger; diff --git a/src/database-controller/src/core/util.js b/src/database-controller/src/core/util.js index a4166240eb..7e9591a663 100644 --- a/src/database-controller/src/core/util.js +++ b/src/database-controller/src/core/util.js @@ -1,68 +1,84 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const logger = require('@dbc/core/logger') +const logger = require('@dbc/core/logger'); -async function timePeriod (ms) { +async function timePeriod(ms) { await new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms) - }) + setTimeout(() => resolve(), ms); + }); } -function alwaysRetryDecorator (promiseFn, loggingMessage, initialRetryDelayMs = 500, backoffRatio = 2, maxRetryDelayMs = 120000) { +function alwaysRetryDecorator( + promiseFn, + loggingMessage, + initialRetryDelayMs = 500, + backoffRatio = 2, + maxRetryDelayMs = 120000, +) { /* promiseFn is an async function This decorator returns a newPromiseFn, which can be run as newPromiseFn(...). The new promise will be always retried. */ - async function _wrapper () { - let nextDelayMs = initialRetryDelayMs - let retryCount = 0 + async function _wrapper() { + let nextDelayMs = initialRetryDelayMs; + let retryCount = 0; while (true) { try { if (retryCount > 0) { - logger.warn(`${loggingMessage} retries=${retryCount}.`) + logger.warn(`${loggingMessage} retries=${retryCount}.`); } - const res = await promiseFn.apply(this, arguments) - logger.info(`${loggingMessage} succeeded.`) - return res + const res = await promiseFn.apply(this, arguments); + logger.info(`${loggingMessage} succeeded.`); + return res; } catch (err) { if (retryCount === 0) { - logger.warn(`${loggingMessage} failed. It will be retried after ${nextDelayMs} ms. Error: ${err.message}`) + logger.warn( + `${loggingMessage} failed. It will be retried after ${nextDelayMs} ms. Error: ${err.message}`, + ); } else { - logger.warn(`${loggingMessage} failed. Retries=${retryCount}. It will be retried after ${nextDelayMs} ms. Error: ${err.message}`) + logger.warn( + `${loggingMessage} failed. Retries=${retryCount}. It will be retried after ${nextDelayMs} ms. Error: ${err.message}`, + ); } - await timePeriod(nextDelayMs) + await timePeriod(nextDelayMs); if (nextDelayMs * backoffRatio < maxRetryDelayMs) { - nextDelayMs = nextDelayMs * backoffRatio + nextDelayMs = nextDelayMs * backoffRatio; } else { - nextDelayMs = maxRetryDelayMs + nextDelayMs = maxRetryDelayMs; } - retryCount += 1 + retryCount += 1; } } } - return _wrapper + return _wrapper; } -function timeoutDecorator (promiseFn, loggingMessage, timeoutMs) { +function timeoutDecorator(promiseFn, loggingMessage, timeoutMs) { /* promiseFn is an async function This decorator returns a newPromiseFn, which can be run as newPromiseFn(...). The new promise will has a timeout */ - async function _wrapper () { + async function _wrapper() { const timeoutPromise = new Promise((resolve, reject) => { - setTimeout(() => reject(new Error(`${loggingMessage} reached timeout ${timeoutMs} ms.`)), timeoutMs) - }) - const resPromise = promiseFn.apply(this, arguments) - const res = await Promise.race([timeoutPromise, resPromise]) - return res + setTimeout( + () => + reject( + new Error(`${loggingMessage} reached timeout ${timeoutMs} ms.`), + ), + timeoutMs, + ); + }); + const resPromise = promiseFn.apply(this, arguments); + const res = await Promise.race([timeoutPromise, resPromise]); + return res; } - return _wrapper + return _wrapper; } module.exports = { alwaysRetryDecorator: alwaysRetryDecorator, - timeoutDecorator: timeoutDecorator -} + timeoutDecorator: timeoutDecorator, +}; diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index efe13eae1f..bf687ddc6b 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -1,49 +1,45 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -require('module-alias/register') -require('dotenv').config() -const DatabaseModel = require('openpaidbsdk') -const fs = require('fs') -const logger = require('@dbc/core/logger') -const { paiVersion, paiCommitVersion } = require('@dbc/package.json') -const k8s = require('@dbc/core/k8s') -const { Snapshot } = require('@dbc/core/framework') +require('module-alias/register'); +require('dotenv').config(); +const DatabaseModel = require('openpaidbsdk'); +const logger = require('@dbc/core/logger'); +const { paiVersion, paiCommitVersion } = require('@dbc/package.json'); +const k8s = require('@dbc/core/k8s'); +const { Snapshot } = require('@dbc/core/framework'); -async function updateFromNoDatabaseVersion (databaseModel) { +async function updateFromNoDatabaseVersion(databaseModel) { // update from 1.0.0 < version < v1.2.0 - await databaseModel.synchronizeSchema() + await databaseModel.synchronizeSchema(); // transfer old frameworks from api server to db - const frameworks = (await k8s.listFramework()).body.items + const frameworks = (await k8s.listFramework()).body.items; for (const framework of frameworks) { - const snapshot = new Snapshot(framework) - logger.info(`Transferring framework ${snapshot.getName()} to database.`) - const record = snapshot.getRecordForLegacyTransfer() - record.requestSynced = true - await databaseModel.Framework.upsert(record) + const snapshot = new Snapshot(framework); + logger.info(`Transferring framework ${snapshot.getName()} to database.`); + const record = snapshot.getRecordForLegacyTransfer(); + record.requestSynced = true; + await databaseModel.Framework.upsert(record); } } // This script should be idempotent. // If any error happens, it should report the error and exit with a non-zero code. // If succeed, it should finish with a zero code. -async function main () { +async function main() { try { - const databaseModel = new DatabaseModel( - process.env.DB_CONNECTION_STR, - 1 - ) - const previousVersion = (await databaseModel.getVersion()).version + const databaseModel = new DatabaseModel(process.env.DB_CONNECTION_STR, 1); + const previousVersion = (await databaseModel.getVersion()).version; if (!previousVersion) { - await updateFromNoDatabaseVersion(databaseModel) + await updateFromNoDatabaseVersion(databaseModel); } - await databaseModel.setVersion(paiVersion, paiCommitVersion) - logger.info('Database has been successfully initialized.') - process.exit(0) + await databaseModel.setVersion(paiVersion, paiCommitVersion); + logger.info('Database has been successfully initialized.'); + process.exit(0); } catch (err) { - logger.error(err) - process.exit(1) + logger.error(err); + process.exit(1); } } -main() +main(); diff --git a/src/database-controller/src/package.json b/src/database-controller/src/package.json index f80c88a6d2..6cc4f24369 100644 --- a/src/database-controller/src/package.json +++ b/src/database-controller/src/package.json @@ -5,7 +5,8 @@ "paiCommitVersion": null, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "lint": "standard.cmd --fix" + "lint": "eslint --ext .js --ext .jsx .", + "lintfix": "eslint --ext .js --ext .jsx . --fix" }, "main": "index.js", "license": "MIT", @@ -31,5 +32,16 @@ }, "_moduleAliases": { "@dbc": "." + }, + "devDependencies": { + "eslint": "6.2.2", + "eslint-config-prettier": "6.1.0", + "eslint-config-standard": "14.0.1", + "eslint-plugin-import": "2.18.2", + "eslint-plugin-node": "9.1.0", + "eslint-plugin-prettier": "3.1.0", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-standard": "4.0.1", + "prettier": "1.18.2" } } diff --git a/src/database-controller/src/poller/config.js b/src/database-controller/src/poller/config.js index ccca9c7882..212e3c9a7b 100644 --- a/src/database-controller/src/poller/config.js +++ b/src/database-controller/src/poller/config.js @@ -1,34 +1,35 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const basicConfig = require('@dbc/core/config') -const _ = require('lodash') -const Joi = require('joi') +const basicConfig = require('@dbc/core/config'); +const _ = require('lodash'); +const Joi = require('joi'); -const configSchema = Joi.object().keys({ - dbConnectionStr: Joi.string() - .required(), - maxDatabaseConnection: Joi.number() - .integer() - .required(), - intervalSecond: Joi.number() - .integer() - .required(), - writeMergerUrl: Joi.string() - .uri() - .required() -}).required() +const configSchema = Joi.object() + .keys({ + dbConnectionStr: Joi.string().required(), + maxDatabaseConnection: Joi.number() + .integer() + .required(), + intervalSecond: Joi.number() + .integer() + .required(), + writeMergerUrl: Joi.string() + .uri() + .required(), + }) + .required(); const config = { dbConnectionStr: process.env.DB_CONNECTION_STR, maxDatabaseConnection: parseInt(process.env.MAX_DB_CONNECTION), intervalSecond: parseInt(process.env.INTERVAL_SECOND), - writeMergerUrl: process.env.WRITE_MERGER_URL -} + writeMergerUrl: process.env.WRITE_MERGER_URL, +}; -const { error, value } = Joi.validate(config, configSchema) +const { error, value } = Joi.validate(config, configSchema); if (error) { - throw new Error(`Config error\n${error}`) + throw new Error(`Config error\n${error}`); } -module.exports = _.assign(basicConfig, value) +module.exports = _.assign(basicConfig, value); diff --git a/src/database-controller/src/poller/index.js b/src/database-controller/src/poller/index.js index da4e6e8598..da722e909c 100644 --- a/src/database-controller/src/poller/index.js +++ b/src/database-controller/src/poller/index.js @@ -1,120 +1,131 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -require('module-alias/register') -require('dotenv').config() -const AsyncLock = require('async-lock') -const { Sequelize } = require('sequelize') -const Op = Sequelize.Op -const DatabaseModel = require('openpaidbsdk') -const logger = require('@dbc/core/logger') -const { Snapshot, AddOns, synchronizeRequest } = require('@dbc/core/framework') -const interval = require('interval-promise') -const config = require('@dbc/poller/config') -const fetch = require('node-fetch') -const { deleteFramework } = require('@dbc/core/k8s') +require('module-alias/register'); +require('dotenv').config(); +const AsyncLock = require('async-lock'); +const { Sequelize } = require('sequelize'); +const Op = Sequelize.Op; +const DatabaseModel = require('openpaidbsdk'); +const logger = require('@dbc/core/logger'); +const { Snapshot, AddOns, synchronizeRequest } = require('@dbc/core/framework'); +const interval = require('interval-promise'); +const config = require('@dbc/poller/config'); +const fetch = require('node-fetch'); +const { deleteFramework } = require('@dbc/core/k8s'); // maxPending is set to 1 to avoid the queue to be too long. // If any framework is not synced/deleted, it will be synced/deleted in the next polling round. -const lock = new AsyncLock({ maxPending: 1 }) +const lock = new AsyncLock({ maxPending: 1 }); const databaseModel = new DatabaseModel( config.dbConnectionStr, - config.maxDatabaseConnection -) + config.maxDatabaseConnection, +); -async function mockDeleteEvent (snapshot) { - await fetch( - `${config.writeMergerUrl}/api/v1/watchEvents/DELETED`, - { - method: 'POST', - body: snapshot.getString(), - headers: { 'Content-Type': 'application/json' }, - timeout: config.writeMergerConnectionTimeoutSecond * 1000 - } - ) +async function mockDeleteEvent(snapshot) { + await fetch(`${config.writeMergerUrl}/api/v1/watchEvents/DELETED`, { + method: 'POST', + body: snapshot.getString(), + headers: { 'Content-Type': 'application/json' }, + timeout: config.writeMergerConnectionTimeoutSecond * 1000, + }); } -function deleteHandler (snapshot, pollingTs) { - const frameworkName = snapshot.getName() - logger.info(`Will delete framework ${frameworkName}. PollingTs=${pollingTs}.`) - lock.acquire(frameworkName, - async () => { +function deleteHandler(snapshot, pollingTs) { + const frameworkName = snapshot.getName(); + logger.info( + `Will delete framework ${frameworkName}. PollingTs=${pollingTs}.`, + ); + lock + .acquire(frameworkName, async () => { try { // We only delete framework here, ignoring the job add-ons. // Because most job add-ons are patched in the creation time, so they will by deleted automatically. // If some add-ons are not successfully patched, they will be deleted by the watch dog service. - await deleteFramework(snapshot.getName()) - logger.info(`Framework ${frameworkName} is successfully deleted. PollingTs=${pollingTs}.`) + await deleteFramework(snapshot.getName()); + logger.info( + `Framework ${frameworkName} is successfully deleted. PollingTs=${pollingTs}.`, + ); } catch (err) { if (err.response && err.response.statusCode === 404) { // for 404 error, mock a delete to write merger - logger.warn(`Cannot find framework ${frameworkName} in API Server. Will mock a deletion to write merger.`) - await mockDeleteEvent(snapshot) + logger.warn( + `Cannot find framework ${frameworkName} in API Server. Will mock a deletion to write merger.`, + ); + await mockDeleteEvent(snapshot); } else { // for non-404 error - throw err + throw err; } } - } - ).catch((err) => { - logger.error( + }) + .catch(err => { + logger.error( `An error happened when delete framework ${frameworkName} and pollingTs=${pollingTs}:`, - err - ) - }) + err, + ); + }); } -function synchronizeHandler (snapshot, addOns, pollingTs) { - const frameworkName = snapshot.getName() - logger.info(`Start synchronizing request of framework ${frameworkName}. PollingTs=${pollingTs}`) - lock.acquire( - frameworkName, - async () => { - await synchronizeRequest( - snapshot, - addOns - ) - logger.info(`Request of framework ${frameworkName} is successfully synchronized. PollingTs=${pollingTs}.`) - } - ).catch((err) => { - logger.error( - `An error happened when synchronize request for framework ${frameworkName} and pollingTs=${pollingTs}:` - , err - ) - }) +function synchronizeHandler(snapshot, addOns, pollingTs) { + const frameworkName = snapshot.getName(); + logger.info( + `Start synchronizing request of framework ${frameworkName}. PollingTs=${pollingTs}`, + ); + lock + .acquire(frameworkName, async () => { + await synchronizeRequest(snapshot, addOns); + logger.info( + `Request of framework ${frameworkName} is successfully synchronized. PollingTs=${pollingTs}.`, + ); + }) + .catch(err => { + logger.error( + `An error happened when synchronize request for framework ${frameworkName} and pollingTs=${pollingTs}:`, + err, + ); + }); } -async function poll () { - const pollingTs = new Date().getTime() +async function poll() { + const pollingTs = new Date().getTime(); try { - logger.info(`Start polling. PollingTs=${pollingTs}`) + logger.info(`Start polling. PollingTs=${pollingTs}`); const frameworks = await databaseModel.Framework.findAll({ - attributes: ['name', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef', 'snapshot', - 'subState', 'requestSynced', 'apiServerDeleted'], + attributes: [ + 'name', + 'configSecretDef', + 'priorityClassDef', + 'dockerSecretDef', + 'snapshot', + 'subState', + 'requestSynced', + 'apiServerDeleted', + ], where: { apiServerDeleted: false, [Op.or]: { subState: 'Completed', - requestSynced: false - } - } - }) + requestSynced: false, + }, + }, + }); for (const framework of frameworks) { - const snapshot = new Snapshot(framework.snapshot) - const addOns = new AddOns(framework.configSecretDef, framework.priorityClassDef, framework.dockerSecretDef) + const snapshot = new Snapshot(framework.snapshot); + const addOns = new AddOns( + framework.configSecretDef, + framework.priorityClassDef, + framework.dockerSecretDef, + ); if (framework.subState === 'Completed') { - deleteHandler(snapshot, pollingTs) + deleteHandler(snapshot, pollingTs); } else { - synchronizeHandler(snapshot, addOns, pollingTs) + synchronizeHandler(snapshot, addOns, pollingTs); } } } catch (err) { - logger.error(`An error happened for pollingTs=${pollingTs}:`, err) - throw err + logger.error(`An error happened for pollingTs=${pollingTs}:`, err); + throw err; } } -interval( - poll, - config.intervalSecond * 1000, - { stopOnError: false } -) +interval(poll, config.intervalSecond * 1000, { stopOnError: false }); diff --git a/src/database-controller/src/prettier.config.js b/src/database-controller/src/prettier.config.js new file mode 100644 index 0000000000..219e71e448 --- /dev/null +++ b/src/database-controller/src/prettier.config.js @@ -0,0 +1,8 @@ +module.exports = { + semi: true, + // Trailing commas help with git merging and conflict resolution + trailingComma: 'all', + // Use single quote in all files. https://github.com/prettier/prettier/issues/1080#issuecomment-390363232 + singleQuote: true, + jsxSingleQuote: true, +}; diff --git a/src/database-controller/src/watcher/framework/config.js b/src/database-controller/src/watcher/framework/config.js index 29aeab9a9e..f75fa8c4b3 100644 --- a/src/database-controller/src/watcher/framework/config.js +++ b/src/database-controller/src/watcher/framework/config.js @@ -1,23 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const basicConfig = require('@dbc/core/config') -const _ = require('lodash') -const Joi = require('joi') +const basicConfig = require('@dbc/core/config'); +const _ = require('lodash'); +const Joi = require('joi'); -const configSchema = Joi.object().keys({ - writeMergerUrl: Joi.string() - .uri() - .required() -}).required() +const configSchema = Joi.object() + .keys({ + writeMergerUrl: Joi.string() + .uri() + .required(), + }) + .required(); const config = { - writeMergerUrl: process.env.WRITE_MERGER_URL -} + writeMergerUrl: process.env.WRITE_MERGER_URL, +}; -const { error, value } = Joi.validate(config, configSchema) +const { error, value } = Joi.validate(config, configSchema); if (error) { - throw new Error(`Config error\n${error}`) + throw new Error(`Config error\n${error}`); } -module.exports = _.assign(basicConfig, value) +module.exports = _.assign(basicConfig, value); diff --git a/src/database-controller/src/watcher/framework/index.js b/src/database-controller/src/watcher/framework/index.js index 92d64f4ac1..c5c2a56a3c 100644 --- a/src/database-controller/src/watcher/framework/index.js +++ b/src/database-controller/src/watcher/framework/index.js @@ -1,29 +1,29 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -require('module-alias/register') -require('dotenv').config() -const fetch = require('node-fetch') -const AsyncLock = require('async-lock') -const logger = require('@dbc/core/logger') -const { getFrameworkInformer } = require('@dbc/core/k8s') -const { alwaysRetryDecorator } = require('@dbc/core/util') -const config = require('@dbc/watcher/framework/config') +require('module-alias/register'); +require('dotenv').config(); +const fetch = require('node-fetch'); +const AsyncLock = require('async-lock'); +const logger = require('@dbc/core/logger'); +const { getFrameworkInformer } = require('@dbc/core/k8s'); +const { alwaysRetryDecorator } = require('@dbc/core/util'); +const config = require('@dbc/watcher/framework/config'); -const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }) +const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }); -async function synchronizeFramework (eventType, apiObject) { +async function synchronizeFramework(eventType, apiObject) { const res = await fetch( `${config.writeMergerUrl}/api/v1/watchEvents/${eventType}`, { method: 'POST', body: JSON.stringify(apiObject), headers: { 'Content-Type': 'application/json' }, - timeout: config.writeMergerConnectionTimeoutSecond * 1000 - } - ) + timeout: config.writeMergerConnectionTimeoutSecond * 1000, + }, + ); if (!res.ok) { - throw new Error(`Request returns a ${res.status} error.`) + throw new Error(`Request returns a ${res.status} error.`); } } @@ -31,26 +31,37 @@ const eventHandler = (eventType, apiObject) => { /* framework name-based lock + always retry */ - const receivedTs = (new Date()).getTime() - const state = (apiObject.status && apiObject.status.state) ? apiObject.status.state : 'Unknown' - logger.info(`Event type=${eventType} receivedTs=${receivedTs} framework=${apiObject.metadata.name} state=${state} received.`) + const receivedTs = new Date().getTime(); + const state = + apiObject.status && apiObject.status.state + ? apiObject.status.state + : 'Unknown'; + logger.info( + `Event type=${eventType} receivedTs=${receivedTs} framework=${apiObject.metadata.name} state=${state} received.`, + ); lock.acquire( apiObject.metadata.name, alwaysRetryDecorator( () => synchronizeFramework(eventType, apiObject), - `Sync to write merger type=${eventType} receivedTs=${receivedTs} framework=${apiObject.metadata.name} state=${state}` - ) - ) -} + `Sync to write merger type=${eventType} receivedTs=${receivedTs} framework=${apiObject.metadata.name} state=${state}`, + ), + ); +}; -const informer = getFrameworkInformer() +const informer = getFrameworkInformer(); -informer.on('add', (apiObject) => { eventHandler('ADDED', apiObject) }) -informer.on('update', (apiObject) => { eventHandler('MODIFED', apiObject) }) -informer.on('delete', (apiObject) => { eventHandler('DELETED', apiObject) }) -informer.on('error', (err) => { +informer.on('add', apiObject => { + eventHandler('ADDED', apiObject); +}); +informer.on('update', apiObject => { + eventHandler('MODIFED', apiObject); +}); +informer.on('delete', apiObject => { + eventHandler('DELETED', apiObject); +}); +informer.on('error', err => { // If any error happens, the process should exit, and let Kubernetes restart it. - logger.error(err) - process.exit(1) -}) -informer.start() + logger.error(err); + process.exit(1); +}); +informer.start(); diff --git a/src/database-controller/src/write-merger/app.js b/src/database-controller/src/write-merger/app.js index 696343a6be..de899db84e 100644 --- a/src/database-controller/src/write-merger/app.js +++ b/src/database-controller/src/write-merger/app.js @@ -1,41 +1,41 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const cors = require('cors') -const morgan = require('morgan') -const express = require('express') -const compress = require('compression') -const bodyParser = require('body-parser') -const logger = require('@dbc/core/logger') -const status = require('statuses') -const handler = require('@dbc/write-merger/handler') -const config = require('@dbc/write-merger/config') +const cors = require('cors'); +const morgan = require('morgan'); +const express = require('express'); +const compress = require('compression'); +const bodyParser = require('body-parser'); +const logger = require('@dbc/core/logger'); +const status = require('statuses'); +const handler = require('@dbc/write-merger/handler'); +const config = require('@dbc/write-merger/config'); -const app = express() +const app = express(); -app.use(cors()) -app.use(compress()) -app.use(bodyParser.urlencoded({ extended: true })) -app.use(bodyParser.json({ limit: config.bodyLimit })) -app.use(bodyParser.text({ type: 'text/*' })) -app.use(morgan('dev', { stream: logger.stream })) +app.use(cors()); +app.use(compress()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json({ limit: config.bodyLimit })); +app.use(bodyParser.text({ type: 'text/*' })); +app.use(morgan('dev', { stream: logger.stream })); -const router = new express.Router() +const router = new express.Router(); -router.route('/ping').get(handler.ping) -router.route('/frameworkRequest').put(handler.receiveFrameworkRequest) -router.route('/watchEvents/:eventType').post(handler.receiveWatchEvents) +router.route('/ping').get(handler.ping); +router.route('/frameworkRequest').put(handler.receiveFrameworkRequest); +router.route('/watchEvents/:eventType').post(handler.receiveWatchEvents); -app.use('/api/v1', router) +app.use('/api/v1', router); // error handling app.use((err, req, res, next) => { - logger.warn(err.stack) - const statusCode = err.statusCode || 500 + logger.warn(err.stack); + const statusCode = err.statusCode || 500; res.status(statusCode).json({ code: status(statusCode), - message: err.message - }) -}) + message: err.message, + }); +}); -module.exports = app +module.exports = app; diff --git a/src/database-controller/src/write-merger/config.js b/src/database-controller/src/write-merger/config.js index 316a6f1192..1259ac94ef 100644 --- a/src/database-controller/src/write-merger/config.js +++ b/src/database-controller/src/write-merger/config.js @@ -1,32 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const basicConfig = require('@dbc/core/config') -const _ = require('lodash') -const Joi = require('joi') +const basicConfig = require('@dbc/core/config'); +const _ = require('lodash'); +const Joi = require('joi'); -const configSchema = Joi.object().keys({ - dbConnectionStr: Joi.string() - .required(), - maxDatabaseConnection: Joi.number() - .integer() - .required(), - bodyLimit: Joi.string() - .default('100mb'), - port: Joi.number() - .integer() - .required() -}).required() +const configSchema = Joi.object() + .keys({ + dbConnectionStr: Joi.string().required(), + maxDatabaseConnection: Joi.number() + .integer() + .required(), + bodyLimit: Joi.string().default('100mb'), + port: Joi.number() + .integer() + .required(), + }) + .required(); const config = { dbConnectionStr: process.env.DB_CONNECTION_STR, maxDatabaseConnection: parseInt(process.env.MAX_DB_CONNECTION), - port: parseInt(process.env.PORT) -} + port: parseInt(process.env.PORT), +}; -const { error, value } = Joi.validate(config, configSchema) +const { error, value } = Joi.validate(config, configSchema); if (error) { - throw new Error(`Config error\n${error}`) + throw new Error(`Config error\n${error}`); } -module.exports = _.assign(basicConfig, value) +module.exports = _.assign(basicConfig, value); diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js index c0a5109cd2..2033281a3e 100644 --- a/src/database-controller/src/write-merger/handler.js +++ b/src/database-controller/src/write-merger/handler.js @@ -1,18 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const createError = require('http-errors') -const logger = require('@dbc/core/logger') -const AsyncLock = require('async-lock') -const DatabaseModel = require('openpaidbsdk') -const config = require('@dbc/write-merger/config') -const { Snapshot, AddOns, silentSynchronizeRequest } = require('@dbc/core/framework') -const _ = require('lodash') -const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }) +const createError = require('http-errors'); +const logger = require('@dbc/core/logger'); +const AsyncLock = require('async-lock'); +const DatabaseModel = require('openpaidbsdk'); +const config = require('@dbc/write-merger/config'); +const { + Snapshot, + AddOns, + silentSynchronizeRequest, +} = require('@dbc/core/framework'); +const _ = require('lodash'); +const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }); const databaseModel = new DatabaseModel( config.dbConnectionStr, - config.maxDatabaseConnection -) + config.maxDatabaseConnection, +); /* For error handling, all handlers follow the same structure: @@ -29,126 +33,144 @@ const databaseModel = new DatabaseModel( */ -async function ping (req, res, next) { +async function ping(req, res, next) { try { - res.status(200).json({ message: 'ok' }) + res.status(200).json({ message: 'ok' }); } catch (err) { - return next(err) + return next(err); } } -async function receiveWatchEvents (req, res, next) { +async function receiveWatchEvents(req, res, next) { try { - const snapshot = new Snapshot(req.body) - const frameworkName = snapshot.getName() + const snapshot = new Snapshot(req.body); + const frameworkName = snapshot.getName(); if (!frameworkName) { - return next(createError(400, 'Cannot find framework name.')) + return next(createError(400, 'Cannot find framework name.')); } await lock.acquire(frameworkName, async () => { const oldFramework = await databaseModel.Framework.findOne({ attributes: ['snapshot'], - where: { name: frameworkName } - } - ) + where: { name: frameworkName }, + }); // database doesn't have the corresponding framework. if (!oldFramework) { if (config.recoveryModeEnabled) { // If database doesn't have the corresponding framework, // and recovery mode is enabled // tolerate the error and create framework in database. - const record = snapshot.getRecordForLegacyTransfer() - record.requestSynced = true - await databaseModel.Framework.create(record) + const record = snapshot.getRecordForLegacyTransfer(); + record.requestSynced = true; + await databaseModel.Framework.create(record); } else { - throw createError(404, `Cannot find framework ${frameworkName}.`) + throw createError(404, `Cannot find framework ${frameworkName}.`); } } else { // Database has the corresponding framework. - const oldSnapshot = new Snapshot(oldFramework.snapshot) - const internalUpdate = {} + const oldSnapshot = new Snapshot(oldFramework.snapshot); + const internalUpdate = {}; if (oldSnapshot.getGeneration() === snapshot.getGeneration()) { // if framework request is equal, mark requestSynced = true - logger.info(`The request of framework ${frameworkName} is synced.`) - internalUpdate.requestSynced = true + logger.info(`The request of framework ${frameworkName} is synced.`); + internalUpdate.requestSynced = true; } else { // if framework request is not equal, // should use framework request in db as ground truth - internalUpdate.requestSynced = false + internalUpdate.requestSynced = false; } // use request in database - snapshot.overrideRequest(oldSnapshot) + snapshot.overrideRequest(oldSnapshot); if (req.params.eventType === 'DELETED') { // if event is DELETED, mark apiServerDeleted = true - internalUpdate.apiServerDeleted = true + internalUpdate.apiServerDeleted = true; } await databaseModel.Framework.update( _.assign(snapshot.getStatusUpdate(), internalUpdate), - { where: { name: frameworkName } } - ) + { where: { name: frameworkName } }, + ); } - }) - res.status(200).json({ message: 'ok' }) + }); + res.status(200).json({ message: 'ok' }); } catch (err) { - return next(err) + return next(err); } } -async function receiveFrameworkRequest (req, res, next) { +async function receiveFrameworkRequest(req, res, next) { try { - const { frameworkRequest, submissionTime, configSecretDef, priorityClassDef, dockerSecretDef } = req.body - const frameworkName = _.get(frameworkRequest, 'metadata.name') + const { + frameworkRequest, + submissionTime, + configSecretDef, + priorityClassDef, + dockerSecretDef, + } = req.body; + const frameworkName = _.get(frameworkRequest, 'metadata.name'); if (!frameworkName) { - return next(createError(400, 'Cannot find framework name.')) + return next(createError(400, 'Cannot find framework name.')); } const [needSynchronize, snapshot, addOns] = await lock.acquire( - frameworkName, async () => { + frameworkName, + async () => { const oldFramework = await databaseModel.Framework.findOne({ attributes: ['snapshot'], - where: { name: frameworkName } - } - ) - const snapshot = new Snapshot(frameworkRequest) - const addOns = new AddOns(configSecretDef, priorityClassDef, dockerSecretDef) + where: { name: frameworkName }, + }); + const snapshot = new Snapshot(frameworkRequest); + const addOns = new AddOns( + configSecretDef, + priorityClassDef, + dockerSecretDef, + ); if (!oldFramework) { // create new record in db // including all add-ons and submissionTime // set requestGeneration = 1 - snapshot.setGeneration(1) - const record = _.assign({}, snapshot.getAllUpdate(), addOns.getUpdate()) - record.submissionTime = new Date(submissionTime) - await databaseModel.Framework.create(record) - return [true, snapshot, addOns] + snapshot.setGeneration(1); + const record = _.assign( + {}, + snapshot.getAllUpdate(), + addOns.getUpdate(), + ); + record.submissionTime = new Date(submissionTime); + await databaseModel.Framework.create(record); + return [true, snapshot, addOns]; } else { // update record in db - const oldSnapshot = new Snapshot(oldFramework.snapshot) + const oldSnapshot = new Snapshot(oldFramework.snapshot); // compare framework request (omit requestGeneration) - if (_.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true))) { + if ( + _.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true)) + ) { // request is equal, no-op - return [false, snapshot, addOns] + return [false, snapshot, addOns]; } else { // request is different // update request in db, mark requestSynced=false - snapshot.setGeneration(oldSnapshot.getGeneration() + 1) + snapshot.setGeneration(oldSnapshot.getGeneration() + 1); await databaseModel.Framework.update( - _.assign({}, snapshot.getRequestUpdate(), { requestSynced: false }), - { where: { name: frameworkName } } - ) - return [true, snapshot, addOns] + _.assign({}, snapshot.getRequestUpdate(), { + requestSynced: false, + }), + { where: { name: frameworkName } }, + ); + return [true, snapshot, addOns]; } } - }) - res.status(200).json({ message: 'ok' }) + }, + ); + res.status(200).json({ message: 'ok' }); // skip db poller, any response or error will be ignored if (needSynchronize) { - silentSynchronizeRequest(snapshot, addOns) + silentSynchronizeRequest(snapshot, addOns); } } catch (err) { - return next(err) + return next(err); } } module.exports = { ping: ping, receiveFrameworkRequest: receiveFrameworkRequest, - receiveWatchEvents: receiveWatchEvents -} + receiveWatchEvents: receiveWatchEvents, +}; diff --git a/src/database-controller/src/write-merger/index.js b/src/database-controller/src/write-merger/index.js index 61bb4717e5..1999795436 100644 --- a/src/database-controller/src/write-merger/index.js +++ b/src/database-controller/src/write-merger/index.js @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -require('module-alias/register') -require('dotenv').config() -const app = require('@dbc/write-merger/app') -const { port } = require('@dbc/write-merger/config') +require('module-alias/register'); +require('dotenv').config(); +const app = require('@dbc/write-merger/app'); +const { port } = require('@dbc/write-merger/config'); -app.listen(port, () => console.log(`Write merger listening on port ${port}!`)) +app.listen(port, () => console.log(`Write merger listening on port ${port}!`)); diff --git a/src/database-controller/src/yarn.lock b/src/database-controller/src/yarn.lock index 2bca50fc56..655d8a9ba3 100644 --- a/src/database-controller/src/yarn.lock +++ b/src/database-controller/src/yarn.lock @@ -2,6 +2,27 @@ # yarn lockfile v1 +"@babel/code-frame@^7.0.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@kubernetes/client-node@0.11.2": version "0.11.2" resolved "https://registry.yarnpkg.com/@kubernetes/client-node/-/client-node-0.11.2.tgz#5dbc1d66d3c954af0f3bd294dece884737f5b761" @@ -85,6 +106,16 @@ accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + +acorn@^7.1.1: + version "7.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" + integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + aggregate-error@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-1.0.0.tgz#888344dad0220a72e3af50906117f48771925fac" @@ -93,7 +124,7 @@ aggregate-error@^1.0.0: clean-stack "^1.0.0" indent-string "^3.0.0" -ajv@^6.5.5: +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.12.3" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== @@ -103,6 +134,28 @@ ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + any-promise@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -120,6 +173,15 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-includes@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" + integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + is-string "^1.0.5" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -132,6 +194,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" @@ -268,16 +335,47 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + clean-stack@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31" integrity sha1-noIVAa6XmYbEax1m0tQy2y/UrjE= +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" + integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== + clone-response@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -293,6 +391,18 @@ cls-bluebird@^2.1.0: is-bluebird "^1.0.2" shimmer "^1.1.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -330,6 +440,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -365,7 +480,7 @@ cors@^2.8.5: object-assign "^4" vary "^1" -cross-spawn@^6.0.0: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -388,14 +503,14 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug@2.6.9: +debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^4.1.1: +debug@^4.0.1, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -414,6 +529,18 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -434,6 +561,21 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dotenv@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" @@ -462,6 +604,11 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -474,6 +621,39 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-regex "^1.1.0" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es6-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -484,11 +664,199 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-config-prettier@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.1.0.tgz#e6f678ba367fbd1273998d5510f76f004e9dce7b" + integrity sha512-k9fny9sPjIBQ2ftFTesJV21Rg4R/7a7t7LCtZVrYQiHEp8Nnuk3EGaDmsKSAnsPj0BYcgB2zxzHa2NTkIxcOLg== + dependencies: + get-stdin "^6.0.0" + +eslint-config-standard@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.0.1.tgz#375c3636fb4bd453cb95321d873de12e4eef790b" + integrity sha512-1RWsAKTDTZgA8bIM6PSC9aTGDAUlKqNkYNJlTZ5xYD/HYkIM6GlcefFvgcJ8xi0SWG5203rttKYX28zW+rKNOg== + +eslint-import-resolver-node@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-module-utils@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + +eslint-plugin-es@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.1.tgz#12acae0f4953e76ba444bfd1b2271081ac620998" + integrity sha512-5fa/gR2yR3NxQf+UXkeLeP8FBBl6tSgdrAz1+cF84v1FMM4twGwQoqTnn+QxFLcPOrF4pdKEJKDB/q9GoyJrCA== + dependencies: + eslint-utils "^1.4.2" + regexpp "^2.0.1" + +eslint-plugin-import@2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" + integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== + dependencies: + array-includes "^3.0.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.4.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.0" + read-pkg-up "^2.0.0" + resolve "^1.11.0" + +eslint-plugin-node@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.1.0.tgz#f2fd88509a31ec69db6e9606d76dabc5adc1b91a" + integrity sha512-ZwQYGm6EoV2cfLpE1wxJWsfnKUIXfM/KM09/TlorkukgCAwmkgajEJnPCmyzoFPQQkmvo5DrW/nyKutNIw36Mw== + dependencies: + eslint-plugin-es "^1.4.0" + eslint-utils "^1.3.1" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-prettier@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz#8695188f95daa93b0dc54b249347ca3b79c4686d" + integrity sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-plugin-promise@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" + integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== + +eslint-plugin-standard@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" + integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== + +eslint-scope@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" + integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.3.1, eslint-utils@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint@6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f" + integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.2" + eslint-visitor-keys "^1.1.0" + espree "^6.1.1" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^11.7.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^6.4.1" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +esquery@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" @@ -548,6 +916,15 @@ extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -568,11 +945,35 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -586,6 +987,27 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -632,6 +1054,21 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -651,7 +1088,14 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@^7.0.0: +glob-parent@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@^7.0.0, glob@^7.1.3: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -663,6 +1107,11 @@ glob@^7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" +globals@^11.7.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + got@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" @@ -686,6 +1135,11 @@ got@^8.3.2: url-parse-lax "^3.0.0" url-to-options "^1.0.1" +graceful-fs@^4.1.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -699,11 +1153,21 @@ har-validator@~5.1.3: ajv "^6.5.5" har-schema "^2.0.0" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + has-symbol-support-x@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw== +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + has-to-string-tag-x@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" @@ -711,11 +1175,23 @@ has-to-string-tag-x@^1.2.0: dependencies: has-symbol-support-x "^1.4.1" +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + hoek@6.x.x: version "6.1.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + http-cache-semantics@3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" @@ -763,7 +1239,7 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -775,6 +1251,29 @@ ieee754@^1.1.4: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.1: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -803,6 +1302,25 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inquirer@^6.4.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" + integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -826,11 +1344,43 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + is-bluebird@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2" integrity sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI= +is-callable@^1.1.4, is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" @@ -841,6 +1391,13 @@ is-plain-obj@^1.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" + integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== + dependencies: + has-symbols "^1.0.1" + is-retry-allowed@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" @@ -851,12 +1408,24 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -isarray@~1.0.0: +isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -900,6 +1469,11 @@ joi@^14.3.1: isemail "3.x.x" topo "3.x.x" +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^3.13.1, js-yaml@^3.14.0: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" @@ -928,6 +1502,11 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -955,12 +1534,38 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + lodash@^4.17.11, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.17.19: +lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.19: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== @@ -1019,6 +1624,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -1031,6 +1641,18 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + module-alias@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" @@ -1074,6 +1696,16 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -1110,6 +1742,16 @@ node-jose@^1.1.0: react-zlib-js "^1.0.4" uuid "^3.3.3" +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-url@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" @@ -1141,6 +1783,36 @@ object-hash@^1.3.1: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== +object-inspect@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.values@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + oidc-token-hash@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-3.0.2.tgz#5bd4716cc48ad433f4e4e99276811019b165697e" @@ -1165,6 +1837,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + openid-client@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-2.5.0.tgz#7d4cf552b30dbad26917d7e2722422eda057ea93" @@ -1179,17 +1858,28 @@ openid-client@2.5.0: oidc-token-hash "^3.0.1" p-any "^1.1.0" -openpaidbsdk@../sdk: +"openpaidbsdk@file:../sdk": version "1.0.0" dependencies: pg "^8.2.1" sequelize "5.21.3" -"openpaidbsdk@file:../sdk": - version "1.0.0" +optionator@^0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: - pg "^8.2.1" - sequelize "5.21.3" + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= p-any@^1.1.0: version "1.1.0" @@ -1213,6 +1903,20 @@ p-is-promise@^1.1.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + p-some@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/p-some/-/p-some-2.0.1.tgz#65d87c8b154edbcf5221d167778b6d2e150f6f06" @@ -1227,6 +1931,11 @@ p-timeout@^2.0.1: dependencies: p-finally "^1.0.0" +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + packet-reader@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" @@ -1237,11 +1946,30 @@ pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -1262,6 +1990,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -1319,11 +2054,23 @@ pgpass@1.x: dependencies: split "^1.0.0" +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -1346,11 +2093,28 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -1361,6 +2125,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -1426,6 +2195,23 @@ react-zlib-js@^1.0.4: resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7" integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A== +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + readable-stream@^2.0.0: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -1446,6 +2232,11 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -1472,7 +2263,12 @@ request@^2.88.0: tunnel-agent "^0.6.0" uuid "^3.3.2" -resolve@^1.1.6: +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.13.1: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -1486,6 +2282,14 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + retry-as-promised@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543" @@ -1498,6 +2302,25 @@ rfc4648@^1.3.0: resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.3.0.tgz#2a69c76f05bc0e388feab933672de9b492af95f1" integrity sha512-x36K12jOflpm1V8QjPq3I+pt7Z1xzeZIjiC8J2Oxd7bE1efTrOG241DTYVJByP/SxR9jl1t7iZqYxDX864jgBQ== +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-async@^2.2.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +rxjs@^6.4.0: + version "6.6.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -1513,17 +2336,17 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +"semver@2 || 3 || 4 || 5", semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= -semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.3.0: +semver@^6.1.0, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -1619,11 +2442,20 @@ shimmer@^1.1.0: resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== -signal-exit@^3.0.0: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -1631,6 +2463,32 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -1678,6 +2536,39 @@ strict-uri-encode@^1.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= +string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -1685,12 +2576,58 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -through@2: +strip-json-comments@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through@2, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -1700,6 +2637,13 @@ timed-out@^4.0.1: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" @@ -1725,7 +2669,7 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tslib@^1.9.3: +tslib@^1.9.0, tslib@^1.9.3: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== @@ -1742,6 +2686,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -1794,6 +2745,19 @@ uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +v8-compile-cache@^2.0.3: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + validator@^10.11.0: version "10.11.0" resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" @@ -1839,11 +2803,23 @@ wkx@^0.4.8: dependencies: "@types/node" "*" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + ws@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" From e608c5ef95cc0656c53eec66e606ba313772e0d8 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Thu, 6 Aug 2020 14:24:30 +0800 Subject: [PATCH 20/32] fix --- .github/workflows/continuous-integration.yml | 2 - .../{sdk => }/.dockerignore | 0 src/database-controller/sdk/package.json | 3 +- src/database-controller/src/.dockerignore | 63 ------------------- src/database-controller/src/package.json | 3 +- src/rest-server/package.json | 3 +- 6 files changed, 4 insertions(+), 70 deletions(-) rename src/database-controller/{sdk => }/.dockerignore (100%) delete mode 100644 src/database-controller/src/.dockerignore diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 990c8b717c..81e800b059 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -131,10 +131,8 @@ jobs: - name: yarn install and test run: | cd src/rest-server - cp -rf ../database-controller/sdk openpaidbsdk yarn config set ignore-engines true yarn install --frozen-lockfiles - rm -rf openpaidbsdk yarn test webportal: diff --git a/src/database-controller/sdk/.dockerignore b/src/database-controller/.dockerignore similarity index 100% rename from src/database-controller/sdk/.dockerignore rename to src/database-controller/.dockerignore diff --git a/src/database-controller/sdk/package.json b/src/database-controller/sdk/package.json index 4af8f555b9..aa918ca671 100644 --- a/src/database-controller/sdk/package.json +++ b/src/database-controller/sdk/package.json @@ -3,8 +3,7 @@ "version": "1.0.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint --ext .js --ext .jsx .", - "lintfix": "eslint --ext .js --ext .jsx . --fix" + "lint": "eslint --ext .js --ext .jsx ." }, "main": "index.js", "license": "MIT", diff --git a/src/database-controller/src/.dockerignore b/src/database-controller/src/.dockerignore deleted file mode 100644 index 1231151c1d..0000000000 --- a/src/database-controller/src/.dockerignore +++ /dev/null @@ -1,63 +0,0 @@ -.git - -# Directory for submitted jobs' json file and scripts -frameworklauncher/ - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env \ No newline at end of file diff --git a/src/database-controller/src/package.json b/src/database-controller/src/package.json index 6cc4f24369..f3dc32973d 100644 --- a/src/database-controller/src/package.json +++ b/src/database-controller/src/package.json @@ -5,8 +5,7 @@ "paiCommitVersion": null, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint --ext .js --ext .jsx .", - "lintfix": "eslint --ext .js --ext .jsx . --fix" + "lint": "eslint --ext .js --ext .jsx ." }, "main": "index.js", "license": "MIT", diff --git a/src/rest-server/package.json b/src/rest-server/package.json index dcb04d53a1..ff442c1edd 100644 --- a/src/rest-server/package.json +++ b/src/rest-server/package.json @@ -71,8 +71,9 @@ "scripts": { "coveralls": "nyc report --reporter=text-lcov | coveralls ..", "lint": "eslint .", - "lintfix": "eslint . --fix", "mocha": "mocha --file ./test/setup --ui bdd --recursive --timeout 1000 --exit", + "preinstall": "cp -rf ../database-controller/sdk openpaidbsdk", + "postinstall": "rm -rf openpaidbsdk", "start": "node index.js", "test": "npm run lint && nyc npm run mocha" } From 979fe5dec7713913581b89a29e1f9411bb43827c Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Fri, 7 Aug 2020 15:33:07 +0800 Subject: [PATCH 21/32] fix --- src/database-controller/sdk/index.js | 13 ++++ .../src/{core => common}/config.js | 0 .../src/{core => common}/framework.js | 73 ++++++++++++++----- .../src/{core => common}/k8s.js | 11 ++- .../src/{core => common}/logger.js | 0 .../src/{core => common}/util.js | 7 +- .../src/initializer/index.js | 22 +++--- src/database-controller/src/poller/config.js | 2 +- src/database-controller/src/poller/index.js | 14 ++-- .../src/watcher/framework/config.js | 2 +- .../src/watcher/framework/index.js | 11 +-- .../src/write-merger/app.js | 2 +- .../src/write-merger/config.js | 2 +- .../src/write-merger/handler.js | 31 +++++--- .../frameworkcontroller-config.yaml.template | 2 +- src/rest-server/package.json | 2 +- 16 files changed, 139 insertions(+), 55 deletions(-) rename src/database-controller/src/{core => common}/config.js (100%) rename src/database-controller/src/{core => common}/framework.js (81%) rename src/database-controller/src/{core => common}/k8s.js (93%) rename src/database-controller/src/{core => common}/logger.js (100%) rename src/database-controller/src/{core => common}/util.js (90%) diff --git a/src/database-controller/sdk/index.js b/src/database-controller/sdk/index.js index 0c8392edcd..244092c5d6 100644 --- a/src/database-controller/sdk/index.js +++ b/src/database-controller/sdk/index.js @@ -15,6 +15,7 @@ class DatabaseModel { class Framework extends Model {} Framework.init( { + // `insertedAt` indicates the time this record is inserted into database. insertedAt: Sequelize.DATE, name: { type: Sequelize.STRING(64), @@ -32,10 +33,15 @@ class DatabaseModel { totalTaskNumber: Sequelize.INTEGER, totalTaskRoleNumber: Sequelize.INTEGER, logPathInfix: Sequelize.STRING(256), + // `submissionTime` indicates the time user submits this job to rest-server. + // It is generated by rest-server, and will be recorded into database. submissionTime: { type: Sequelize.DATE, allowNull: false, }, + // `dockerSecretDef`, `configSecretDef`, and `priorityClassDef` is the definition of job add-ons. + // They are generated by rest-server and recorded into database by write-merger. + // These add-ons are created by poller or the short-cut in write-merger. dockerSecretDef: Sequelize.TEXT, configSecretDef: Sequelize.TEXT, priorityClassDef: Sequelize.TEXT, @@ -49,16 +55,23 @@ class DatabaseModel { subState: Sequelize.STRING(32), state: Sequelize.STRING(32), snapshot: Sequelize.TEXT, + // `requestSynced`` indicates whether the framework request has been synced with the API server. + // A framework request is not synced by default. + // When the write merger finds the request in the watched events is the same as the one in database, + // it will set requestSynced=true. requestSynced: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false, }, + // `apiServerDeleted` indicates whether the framework is deleted in the API server. + // When the poller finds a framework is completed, it will delete it from the API server. apiServerDeleted: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false, }, + // If a job is archived, it will not be shown in the LIST job API by default. archived: { type: Sequelize.BOOLEAN, allowNull: false, diff --git a/src/database-controller/src/core/config.js b/src/database-controller/src/common/config.js similarity index 100% rename from src/database-controller/src/core/config.js rename to src/database-controller/src/common/config.js diff --git a/src/database-controller/src/core/framework.js b/src/database-controller/src/common/framework.js similarity index 81% rename from src/database-controller/src/core/framework.js rename to src/database-controller/src/common/framework.js index 9ae8f8baa9..a7cbb343b6 100644 --- a/src/database-controller/src/core/framework.js +++ b/src/database-controller/src/common/framework.js @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const logger = require('@dbc/core/logger'); -const k8s = require('@dbc/core/k8s'); +const logger = require('@dbc/common/logger'); +const k8s = require('@dbc/common/k8s'); const _ = require('lodash'); const yaml = require('js-yaml'); +const zlib = require('zlib'); const mockFrameworkStatus = () => { return { @@ -21,7 +22,7 @@ const mockFrameworkStatus = () => { }; }; -const convertState = (state, exitCode, retryDelaySec) => { +const convertFrameworkState = (state, exitCode, retryDelaySec) => { switch (state) { case 'AttemptCreationPending': case 'AttemptCreationRequested': @@ -56,15 +57,25 @@ const convertState = (state, exitCode, retryDelaySec) => { } }; -function ignoreError(err) { +const decompressField = (val) => { + if (val == null) { + return null; + } else { + return JSON.parse(zlib.gunzipSync(Buffer.from(val, 'base64')).toString()); + } +}; + +function logError(err) { logger.info('This error will be ignored: ', err); } // Class `Snapshot` handles the full json of framework. // It provides method like: // getRequest: extract framework request from the full json -// overrideRequest: override the framework request using another snapshot +// overrideRequest: override the framework request to be another snapshot's framework request // getRequestUpdate, getStatusUpdate, getAllUpdate: Get database updates from the snapshot. +// They are used to update database records. e.g. If we want to update the framework request +// in database, we can do dbModel.update(snapshot.getRequestUpdate(), where: {name: snapshot.getName()}) // It doesn't handle database internal status, like: requestSynced, apiServerDeleted, ..., etc. class Snapshot { constructor(snapshot) { @@ -85,6 +96,7 @@ class Snapshot { } getRequest(omitGeneration) { + // extract framework request from the full json const request = _.pick(this._snapshot, [ 'apiVersion', 'kind', @@ -94,6 +106,9 @@ class Snapshot { 'spec', ]); if (omitGeneration) { + // User submits framework request to database, and compare this request with the one in database. + // If the request is the same, no-op. Otherwise set `requestGeneration` = `requestGeneration` + 1. + // When this kind of comparison happens, we should omit the current `requestGeneration`. return _.omit(request, 'metadata.annotations.requestGeneration'); } else { return request; @@ -101,6 +116,7 @@ class Snapshot { } overrideRequest(otherSnapshot) { + // override the framework request to be another snapshot's framework request // shouldn't use _.merge here _.assign( this._snapshot, @@ -116,7 +132,19 @@ class Snapshot { ); } + unzipTaskRoleStatuses() { + // Sometimes, `taskRoleStatuses` is too large and can be compressed. + // This function decompress this field. + // It is usually called before we write snapshot into database. + const attemptStatus = this._snapshot.status.attemptStatus; + if ((!attemptStatus.taskRoleStatuses) && (attemptStatus.taskRoleStatusesCompressed)) { + attemptStatus.taskRoleStatuses = decompressField(attemptStatus.taskRoleStatusesCompressed); + attemptStatus.taskRoleStatusesCompressed = null; + } + } + getRequestUpdate(withSnapshot = true) { + // Get database updates from the snapshot for the request part. const loadedConfig = yaml.safeLoad( this._snapshot.metadata.annotations.config, ); @@ -143,12 +171,14 @@ class Snapshot { logPathInfix: this._snapshot.metadata.annotations.logPathInfix, }; if (withSnapshot) { + this.unzipTaskRoleStatuses() update.snapshot = JSON.stringify(this._snapshot); } return update; } getStatusUpdate(withSnapshot = true) { + // Get database updates from the snapshot for the status part. const completionStatus = this._snapshot.status.attemptStatus .completionStatus; const update = { @@ -168,25 +198,28 @@ class Snapshot { : null, appExitCode: completionStatus ? completionStatus.code : null, subState: this._snapshot.status.state, - state: convertState( + state: convertFrameworkState( this._snapshot.status.state, completionStatus ? completionStatus.code : null, this._snapshot.status.retryPolicyStatus.retryDelaySec, ), }; if (withSnapshot) { + this.unzipTaskRoleStatuses() update.snapshot = JSON.stringify(this._snapshot); } return update; } getAllUpdate(withSnapshot = true) { + // Get database updates from the snapshot for both framework request and status part. const update = _.assign( {}, this.getRequestUpdate(false), this.getStatusUpdate(false), ); if (withSnapshot) { + this.unzipTaskRoleStatuses() update.snapshot = JSON.stringify(this._snapshot); } return update; @@ -200,7 +233,7 @@ class Snapshot { } else { record.submissionTime = new Date(); } - this.setGeneration(1); + this.setRequestGeneration(1); return record; } @@ -208,6 +241,10 @@ class Snapshot { return this._snapshot.metadata.name; } + getState() { + return this._snapshot.status.state; + } + getSnapshot() { return _.cloneDeep(this._snapshot); } @@ -232,14 +269,16 @@ class Snapshot { } } - setGeneration(generation) { + setRequestGeneration(generation) { this._snapshot.metadata.annotations.requestGeneration = generation.toString(); } - getGeneration() { + getRequestGeneration() { + // `requestGeneration` is used to track framework request changes and determine whether it is synced with API server. + // If `requestGeneration` in database equals the one from API server, we will mark the database field `requestSynced` = true. if (!_.has(this._snapshot, 'metadata.annotations.requestGeneration')) { - // for some legacy jobs, use 1 as its request generation. - this.setGeneration(1); + // for some legacy jobs, use 1 as its requestGeneration. + this.setRequestGeneration(1); } return parseInt(this._snapshot.metadata.annotations.requestGeneration); } @@ -317,23 +356,23 @@ class AddOns { this._configSecretDef && k8s .patchSecretOwnerToFramework(this._configSecretDef, frameworkResponse) - .catch(ignoreError); + .catch(logError); this._dockerSecretDef && k8s .patchSecretOwnerToFramework(this._dockerSecretDef, frameworkResponse) - .catch(ignoreError); + .catch(logError); } silentDelete() { // do not await for delete this._configSecretDef && - k8s.deleteSecret(this._configSecretDef.metadata.name).catch(ignoreError); + k8s.deleteSecret(this._configSecretDef.metadata.name).catch(logError); this._priorityClassDef && k8s .deletePriorityClass(this._priorityClassDef.metadata.name) - .catch(ignoreError); + .catch(logError); this._dockerSecretDef && - k8s.deleteSecret(this._dockerSecretDef.metadata.name).catch(ignoreError); + k8s.deleteSecret(this._dockerSecretDef.metadata.name).catch(logError); } getUpdate() { @@ -410,7 +449,7 @@ async function synchronizeRequest(snapshot, addOns) { function silentSynchronizeRequest(snapshot, addOns) { // any error will be ignored - synchronizeRequest(snapshot, addOns).catch(ignoreError); + synchronizeRequest(snapshot, addOns).catch(logError); } module.exports = { diff --git a/src/database-controller/src/core/k8s.js b/src/database-controller/src/common/k8s.js similarity index 93% rename from src/database-controller/src/core/k8s.js rename to src/database-controller/src/common/k8s.js index 3a6621dd8f..5ec2125444 100644 --- a/src/database-controller/src/core/k8s.js +++ b/src/database-controller/src/common/k8s.js @@ -78,6 +78,10 @@ async function createFramework(frameworkDescription, namespace = 'default') { } async function patchFramework(name, data, namespace = 'default') { + if (data.status) { + logger.warn('Modifying status field in framework is not allowed! Will delete it.') + delete data.status + } const res = await customObjectsClient.patchNamespacedCustomObject( 'frameworkcontroller.microsoft.com', 'v1', @@ -97,6 +101,9 @@ async function deleteFramework(name, namespace = 'default') { namespace, 'frameworks', name, + ...Array(4), // skip some parameters + { headers: { 'propagationPolicy': 'Foreground' } }, + ); return res.response; } @@ -118,7 +125,7 @@ function getFrameworkInformer( If the informer disconnects normally from API server, it will re-connect automatically. But during the reconnection, listFn will be called again, which is inefficient. According to https://github.com/kubernetes-client/javascript/blob/932c2fbc34db954c6ed397b3cd9ead08b2ff1d10/src/cache.ts#L82-L85, - this behavior will be fixed in the future. + this behavior will be fixed in the future. For this version, we set a large timeout to mitigate this issue. TO DO: If @kubernetes/client-node fixes this issue, we should upgrade our code to use the new code. If the informer encounters any error, it will stop watching, and won't re-connect. @@ -136,7 +143,7 @@ function getFrameworkInformer( }; const informer = k8s.makeInformer( kc, - `/apis/frameworkcontroller.microsoft.com/v1/frameworks?timeoutSeconds=${timeoutSeconds}`, + `/apis/frameworkcontroller.microsoft.com/v1/namespaces/${namespace}/frameworks?timeoutSeconds=${timeoutSeconds}`, listFn, ); return informer; diff --git a/src/database-controller/src/core/logger.js b/src/database-controller/src/common/logger.js similarity index 100% rename from src/database-controller/src/core/logger.js rename to src/database-controller/src/common/logger.js diff --git a/src/database-controller/src/core/util.js b/src/database-controller/src/common/util.js similarity index 90% rename from src/database-controller/src/core/util.js rename to src/database-controller/src/common/util.js index 7e9591a663..89761e1769 100644 --- a/src/database-controller/src/core/util.js +++ b/src/database-controller/src/common/util.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const logger = require('@dbc/core/logger'); +const logger = require('@dbc/common/logger'); async function timePeriod(ms) { await new Promise((resolve, reject) => { @@ -15,6 +15,7 @@ function alwaysRetryDecorator( initialRetryDelayMs = 500, backoffRatio = 2, maxRetryDelayMs = 120000, + randomizeDelay = true, ) { /* promiseFn is an async function @@ -48,6 +49,10 @@ function alwaysRetryDecorator( } else { nextDelayMs = maxRetryDelayMs; } + if (randomizeDelay) { + // randomize between nextDelayMs * (0.8 ~ 1.2) + nextDelayMs = nextDelayMs * (Math.random() * 0.4 + 0.8) + } retryCount += 1; } } diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index bf687ddc6b..d4160e6274 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -4,23 +4,25 @@ require('module-alias/register'); require('dotenv').config(); const DatabaseModel = require('openpaidbsdk'); -const logger = require('@dbc/core/logger'); +const logger = require('@dbc/common/logger'); const { paiVersion, paiCommitVersion } = require('@dbc/package.json'); -const k8s = require('@dbc/core/k8s'); -const { Snapshot } = require('@dbc/core/framework'); +const k8s = require('@dbc/common/k8s'); +const { Snapshot } = require('@dbc/common/framework'); async function updateFromNoDatabaseVersion(databaseModel) { // update from 1.0.0 < version < v1.2.0 await databaseModel.synchronizeSchema(); // transfer old frameworks from api server to db const frameworks = (await k8s.listFramework()).body.items; + const upsertPromises = []; for (const framework of frameworks) { const snapshot = new Snapshot(framework); logger.info(`Transferring framework ${snapshot.getName()} to database.`); const record = snapshot.getRecordForLegacyTransfer(); record.requestSynced = true; - await databaseModel.Framework.upsert(record); + upsertPromises.push(databaseModel.Framework.upsert(record)); } + await Promise.all(upsertPromises) } // This script should be idempotent. @@ -28,17 +30,19 @@ async function updateFromNoDatabaseVersion(databaseModel) { // If succeed, it should finish with a zero code. async function main() { try { - const databaseModel = new DatabaseModel(process.env.DB_CONNECTION_STR, 1); + const databaseModel = new DatabaseModel(process.env.DB_CONNECTION_STR, 50); const previousVersion = (await databaseModel.getVersion()).version; if (!previousVersion) { await updateFromNoDatabaseVersion(databaseModel); } await databaseModel.setVersion(paiVersion, paiCommitVersion); - logger.info('Database has been successfully initialized.'); - process.exit(0); + logger.info('Database has been successfully initialized.', function () { + process.exit(0); + }); } catch (err) { - logger.error(err); - process.exit(1); + logger.error(err, function () { + process.exit(1); + }); } } diff --git a/src/database-controller/src/poller/config.js b/src/database-controller/src/poller/config.js index 212e3c9a7b..90fc9d6078 100644 --- a/src/database-controller/src/poller/config.js +++ b/src/database-controller/src/poller/config.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const basicConfig = require('@dbc/core/config'); +const basicConfig = require('@dbc/common/config'); const _ = require('lodash'); const Joi = require('joi'); diff --git a/src/database-controller/src/poller/index.js b/src/database-controller/src/poller/index.js index da722e909c..96a9b18f50 100644 --- a/src/database-controller/src/poller/index.js +++ b/src/database-controller/src/poller/index.js @@ -7,21 +7,22 @@ const AsyncLock = require('async-lock'); const { Sequelize } = require('sequelize'); const Op = Sequelize.Op; const DatabaseModel = require('openpaidbsdk'); -const logger = require('@dbc/core/logger'); -const { Snapshot, AddOns, synchronizeRequest } = require('@dbc/core/framework'); +const logger = require('@dbc/common/logger'); +const { Snapshot, AddOns, synchronizeRequest } = require('@dbc/common/framework'); const interval = require('interval-promise'); const config = require('@dbc/poller/config'); const fetch = require('node-fetch'); -const { deleteFramework } = require('@dbc/core/k8s'); +const { deleteFramework } = require('@dbc/common/k8s'); // maxPending is set to 1 to avoid the queue to be too long. // If any framework is not synced/deleted, it will be synced/deleted in the next polling round. +// We don't need to explicitly retry on error. const lock = new AsyncLock({ maxPending: 1 }); const databaseModel = new DatabaseModel( config.dbConnectionStr, config.maxDatabaseConnection, ); -async function mockDeleteEvent(snapshot) { +async function postMockedDeleteEvent(snapshot) { await fetch(`${config.writeMergerUrl}/api/v1/watchEvents/DELETED`, { method: 'POST', body: snapshot.getString(), @@ -49,9 +50,10 @@ function deleteHandler(snapshot, pollingTs) { if (err.response && err.response.statusCode === 404) { // for 404 error, mock a delete to write merger logger.warn( - `Cannot find framework ${frameworkName} in API Server. Will mock a deletion to write merger.`, + `Cannot find framework ${frameworkName} in API Server. Will mock a deletion to write merger. Error:`, + err, ); - await mockDeleteEvent(snapshot); + await postMockedDeleteEvent(snapshot); } else { // for non-404 error throw err; diff --git a/src/database-controller/src/watcher/framework/config.js b/src/database-controller/src/watcher/framework/config.js index f75fa8c4b3..dc3599b085 100644 --- a/src/database-controller/src/watcher/framework/config.js +++ b/src/database-controller/src/watcher/framework/config.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const basicConfig = require('@dbc/core/config'); +const basicConfig = require('@dbc/common/config'); const _ = require('lodash'); const Joi = require('joi'); diff --git a/src/database-controller/src/watcher/framework/index.js b/src/database-controller/src/watcher/framework/index.js index c5c2a56a3c..6035b7c7d9 100644 --- a/src/database-controller/src/watcher/framework/index.js +++ b/src/database-controller/src/watcher/framework/index.js @@ -5,9 +5,9 @@ require('module-alias/register'); require('dotenv').config(); const fetch = require('node-fetch'); const AsyncLock = require('async-lock'); -const logger = require('@dbc/core/logger'); -const { getFrameworkInformer } = require('@dbc/core/k8s'); -const { alwaysRetryDecorator } = require('@dbc/core/util'); +const logger = require('@dbc/common/logger'); +const { getFrameworkInformer } = require('@dbc/common/k8s'); +const { alwaysRetryDecorator } = require('@dbc/common/util'); const config = require('@dbc/watcher/framework/config'); const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }); @@ -61,7 +61,8 @@ informer.on('delete', apiObject => { }); informer.on('error', err => { // If any error happens, the process should exit, and let Kubernetes restart it. - logger.error(err); - process.exit(1); + logger.error(err, function() { + process.exit(1); + }); }); informer.start(); diff --git a/src/database-controller/src/write-merger/app.js b/src/database-controller/src/write-merger/app.js index de899db84e..62b07ba896 100644 --- a/src/database-controller/src/write-merger/app.js +++ b/src/database-controller/src/write-merger/app.js @@ -6,7 +6,7 @@ const morgan = require('morgan'); const express = require('express'); const compress = require('compression'); const bodyParser = require('body-parser'); -const logger = require('@dbc/core/logger'); +const logger = require('@dbc/common/logger'); const status = require('statuses'); const handler = require('@dbc/write-merger/handler'); const config = require('@dbc/write-merger/config'); diff --git a/src/database-controller/src/write-merger/config.js b/src/database-controller/src/write-merger/config.js index 1259ac94ef..d3af013482 100644 --- a/src/database-controller/src/write-merger/config.js +++ b/src/database-controller/src/write-merger/config.js @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const basicConfig = require('@dbc/core/config'); +const basicConfig = require('@dbc/common/config'); const _ = require('lodash'); const Joi = require('joi'); diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js index 2033281a3e..4be9e58a9c 100644 --- a/src/database-controller/src/write-merger/handler.js +++ b/src/database-controller/src/write-merger/handler.js @@ -2,7 +2,7 @@ // Licensed under the MIT License. const createError = require('http-errors'); -const logger = require('@dbc/core/logger'); +const logger = require('@dbc/common/logger'); const AsyncLock = require('async-lock'); const DatabaseModel = require('openpaidbsdk'); const config = require('@dbc/write-merger/config'); @@ -10,7 +10,7 @@ const { Snapshot, AddOns, silentSynchronizeRequest, -} = require('@dbc/core/framework'); +} = require('@dbc/common/framework'); const _ = require('lodash'); const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }); const databaseModel = new DatabaseModel( @@ -69,7 +69,7 @@ async function receiveWatchEvents(req, res, next) { // Database has the corresponding framework. const oldSnapshot = new Snapshot(oldFramework.snapshot); const internalUpdate = {}; - if (oldSnapshot.getGeneration() === snapshot.getGeneration()) { + if (oldSnapshot.getRequestGeneration() === snapshot.getRequestGeneration()) { // if framework request is equal, mark requestSynced = true logger.info(`The request of framework ${frameworkName} is synced.`); internalUpdate.requestSynced = true; @@ -81,8 +81,17 @@ async function receiveWatchEvents(req, res, next) { // use request in database snapshot.overrideRequest(oldSnapshot); if (req.params.eventType === 'DELETED') { - // if event is DELETED, mark apiServerDeleted = true - internalUpdate.apiServerDeleted = true; + if (snapshot.getState() === 'Completed') { + // if event is DELETED and the state is Completed, mark apiServerDeleted = true + internalUpdate.apiServerDeleted = true; + } else { + // Event is DELETED and the state is not Completed. + // This case could occur when someone deletes the framework in API server directly. + // In such case, we mark requestSynced=false, and reset the snapshot using the framework request. + snapshot = new Snapshot(snapshot.getRequest(false)) + internalUpdate.requestSynced = false; + internalUpdate.apiServerDeleted = false; + } } await databaseModel.Framework.update( _.assign(snapshot.getStatusUpdate(), internalUpdate), @@ -126,9 +135,9 @@ async function receiveFrameworkRequest(req, res, next) { // create new record in db // including all add-ons and submissionTime // set requestGeneration = 1 - snapshot.setGeneration(1); + snapshot.setRequestGeneration(1); const record = _.assign( - {}, + {requestSynced: false, apiServerDeleted: false}, snapshot.getAllUpdate(), addOns.getUpdate(), ); @@ -147,7 +156,7 @@ async function receiveFrameworkRequest(req, res, next) { } else { // request is different // update request in db, mark requestSynced=false - snapshot.setGeneration(oldSnapshot.getGeneration() + 1); + snapshot.setRequestGeneration(oldSnapshot.getRequestGeneration() + 1); await databaseModel.Framework.update( _.assign({}, snapshot.getRequestUpdate(), { requestSynced: false, @@ -160,7 +169,11 @@ async function receiveFrameworkRequest(req, res, next) { }, ); res.status(200).json({ message: 'ok' }); - // skip db poller, any response or error will be ignored + // Poller has an interval to synchronize the framework request to API server. + // The interval will cause a delay on every job. + // So we introduce a short-cut here to synchronize the framework request. + // It is an async function call and doesn't affect the return of this function. + // Any error of it will be logged and ignored. if (needSynchronize) { silentSynchronizeRequest(snapshot, addOns); } diff --git a/src/frameworkcontroller/deploy/frameworkcontroller-config.yaml.template b/src/frameworkcontroller/deploy/frameworkcontroller-config.yaml.template index 66aa1b1075..3847ee845c 100644 --- a/src/frameworkcontroller/deploy/frameworkcontroller-config.yaml.template +++ b/src/frameworkcontroller/deploy/frameworkcontroller-config.yaml.template @@ -29,7 +29,7 @@ data: largeFrameworkCompression: true - #frameworkCompletedRetainSec: 2592000 + frameworkCompletedRetainSec: 2147483600 #frameworkMinRetryDelaySecForTransientConflictFailed: 60 #frameworkMaxRetryDelaySecForTransientConflictFailed: 900 diff --git a/src/rest-server/package.json b/src/rest-server/package.json index ff442c1edd..0a331798eb 100644 --- a/src/rest-server/package.json +++ b/src/rest-server/package.json @@ -72,7 +72,7 @@ "coveralls": "nyc report --reporter=text-lcov | coveralls ..", "lint": "eslint .", "mocha": "mocha --file ./test/setup --ui bdd --recursive --timeout 1000 --exit", - "preinstall": "cp -rf ../database-controller/sdk openpaidbsdk", + "preinstall": "npx ncp ../database-controller/sdk openpaidbsdk || echo skip copying openpaidbsdk", "postinstall": "rm -rf openpaidbsdk", "start": "node index.js", "test": "npm run lint && nyc npm run mocha" From 94d8ae9a488e046e5553a67dbe028549d9e6228b Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Fri, 7 Aug 2020 18:00:26 +0800 Subject: [PATCH 22/32] fix --- .../src/common/framework.js | 17 +- src/database-controller/src/package.json | 1 + .../src/write-merger/app.js | 7 +- .../src/write-merger/handler.js | 171 +++++++++++----- src/database-controller/src/yarn.lock | 182 +++++++++++++++++- src/rest-server/src/models/v2/job/k8s.js | 36 +--- 6 files changed, 330 insertions(+), 84 deletions(-) diff --git a/src/database-controller/src/common/framework.js b/src/database-controller/src/common/framework.js index a7cbb343b6..d259c12999 100644 --- a/src/database-controller/src/common/framework.js +++ b/src/database-controller/src/common/framework.js @@ -6,6 +6,7 @@ const k8s = require('@dbc/common/k8s'); const _ = require('lodash'); const yaml = require('js-yaml'); const zlib = require('zlib'); +const jsonmergepatch = require('json-merge-patch'); const mockFrameworkStatus = () => { return { @@ -282,6 +283,14 @@ class Snapshot { } return parseInt(this._snapshot.metadata.annotations.requestGeneration); } + + applyRequestPatch(patchData) { + if (patchData.status) { + // doesn't allow patch status + delete patchData.status; + } + this._snapshot = jsonmergepatch.apply(this._snapshot, patchData); + } } // Class Add-ons handles creation/patching/deletion of job add-ons. @@ -448,8 +457,12 @@ async function synchronizeRequest(snapshot, addOns) { } function silentSynchronizeRequest(snapshot, addOns) { - // any error will be ignored - synchronizeRequest(snapshot, addOns).catch(logError); + try { + // any error will be ignored + synchronizeRequest(snapshot, addOns).catch(logError); + } catch (err) { + logError(err) + } } module.exports = { diff --git a/src/database-controller/src/package.json b/src/database-controller/src/package.json index f3dc32973d..df6f6c3b56 100644 --- a/src/database-controller/src/package.json +++ b/src/database-controller/src/package.json @@ -21,6 +21,7 @@ "interval-promise": "^1.4.0", "joi": "^14.3.1", "js-yaml": "^3.14.0", + "json-merge-patch": "^1.0.0", "lodash": "^4.17.19", "module-alias": "^2.2.2", "morgan": "^1.10.0", diff --git a/src/database-controller/src/write-merger/app.js b/src/database-controller/src/write-merger/app.js index 62b07ba896..8d364c2893 100644 --- a/src/database-controller/src/write-merger/app.js +++ b/src/database-controller/src/write-merger/app.js @@ -16,15 +16,16 @@ const app = express(); app.use(cors()); app.use(compress()); app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ limit: config.bodyLimit })); +app.use(bodyParser.json({ limit: config.bodyLimit, type: 'application/*'})); app.use(bodyParser.text({ type: 'text/*' })); app.use(morgan('dev', { stream: logger.stream })); const router = new express.Router(); router.route('/ping').get(handler.ping); -router.route('/frameworkRequest').put(handler.receiveFrameworkRequest); -router.route('/watchEvents/:eventType').post(handler.receiveWatchEvents); +router.route('/frameworkRequest/:frameworkName').put(handler.putFrameworkRequest); +router.route('/frameworkRequest/:frameworkName').patch(handler.patchFrameworkRequest); +router.route('/watchEvents/:eventType').post(handler.postWatchEvents); app.use('/api/v1', router); diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js index 4be9e58a9c..adf538437f 100644 --- a/src/database-controller/src/write-merger/handler.js +++ b/src/database-controller/src/write-merger/handler.js @@ -41,7 +41,7 @@ async function ping(req, res, next) { } } -async function receiveWatchEvents(req, res, next) { +async function postWatchEvents(req, res, next) { try { const snapshot = new Snapshot(req.body); const frameworkName = snapshot.getName(); @@ -105,7 +105,105 @@ async function receiveWatchEvents(req, res, next) { } } -async function receiveFrameworkRequest(req, res, next) { +async function onCreateFrameworkRequest(snapshot, submissionTime, addOns) { + // create new record in db + // including all add-ons and submissionTime + // set requestGeneration = 1 + snapshot.setRequestGeneration(1); + const record = _.assign( + {requestSynced: false, apiServerDeleted: false}, + snapshot.getAllUpdate(), + addOns.getUpdate(), + ); + record.submissionTime = new Date(submissionTime); + await databaseModel.Framework.create(record); + // Poller has an interval to synchronize the framework request to API server. + // The interval will cause a delay on every job. + // So we introduce a short-cut here to synchronize the framework request. + // It is an async function call and doesn't affect the return of this function. + // Any error of it will be logged and ignored. + silentSynchronizeRequest(snapshot, addOns); +} + +async function onModifyFrameworkRequest(oldSnapshot, snapshot, addOns) { + // compare framework request (omit requestGeneration) + if ( + _.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true)) + ) { + // request is equal, no-op + } else { + // request is different + // update request in db, mark requestSynced=false + snapshot.setRequestGeneration(oldSnapshot.getRequestGeneration() + 1); + await databaseModel.Framework.update( + _.assign({}, snapshot.getRequestUpdate(), { + requestSynced: false, + }), + { where: { name: snapshot.getName() } }, + ); + // Poller has an interval to synchronize the framework request to API server. + // The interval will cause a delay on every job. + // So we introduce a short-cut here to synchronize the framework request. + // It is an async function call and doesn't affect the return of this function. + // Any error of it will be logged and ignored. + silentSynchronizeRequest(snapshot, addOns); + } +} + +async function patchFrameworkRequest(req, res, next) { + // The handler to handle PATCH /frameworkRequest. + // PATCH means provide a part of data, and the current framework request should be updated according to the patch. + // We use the rules of "JSON merge patch" for request modifying. + // If the framework request JSON is changed, we will mark it as requestSynced=false. + // A requestSynced=false request will be synchronized to API server (no matter whether it is completed). + try { + const patchData = req.body; + const frameworkName = req.params.frameworkName; + if (!frameworkName) { + return next(createError(400, 'Cannot find framework name.')); + } + if (_.has(patchData, 'metadata.name') && patchData.metadata.name !== frameworkName) { + return next(createError(400, 'The framework names in query string doesn\'t match the name in body.')) + } + await lock.acquire( + frameworkName, + async () => { + const oldFramework = await databaseModel.Framework.findOne({ + attributes: ['snapshot', 'submissionTime', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef'], + where: { name: frameworkName }, + }); + if (!oldFramework) { + // if the old framework doesn't exist, throw a 404 error. + throw createError(404, `Cannot find framework ${frameworkName}.`); + } else { + // if the old framework exists + const oldSnapshot = new Snapshot(oldFramework.snapshot) + const snapshot = oldSnapshot.copy() + snapshot.applyRequestPatch(patchData); + const addOns = new AddOns( + oldFramework.configSecretDef, + oldFramework.priorityClassDef, + oldFramework.dockerSecretDef, + ); + return onModifyFrameworkRequest(oldSnapshot, snapshot, addOns) + } + }, + ); + res.status(200).json({ message: 'ok' }); + } catch (err) { + return next(err); + } + +} + +async function putFrameworkRequest(req, res, next) { + // The handler to handle PUT /frameworkRequest. + // PUT means provide a full spec of framework request, and the corresponding request will be created or updated. + // Along with the framework request, user must provide other job add-ons, e.g. configSecretDef, priorityClassDef, dockerSecretDef. + // If the framework doesn't exist in database, the record will be created. + // If the framework already exists, the record will be updated, and all job add-ons will be ignored. (Job add-ons can't be changed). + // If the framework request JSON is changed(or created), we will mark it as requestSynced=false. + // A requestSynced=false request will be synchronized to API server (no matter whether it is completed). try { const { frameworkRequest, @@ -118,65 +216,39 @@ async function receiveFrameworkRequest(req, res, next) { if (!frameworkName) { return next(createError(400, 'Cannot find framework name.')); } - const [needSynchronize, snapshot, addOns] = await lock.acquire( + if (req.params.frameworkName !== frameworkName) { + return next(createError(400, 'The framework names in query string doesn\'t match the name in body.')) + } + await lock.acquire( frameworkName, async () => { const oldFramework = await databaseModel.Framework.findOne({ - attributes: ['snapshot'], + attributes: ['snapshot', 'submissionTime', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef'], where: { name: frameworkName }, }); const snapshot = new Snapshot(frameworkRequest); - const addOns = new AddOns( - configSecretDef, - priorityClassDef, - dockerSecretDef, - ); if (!oldFramework) { - // create new record in db - // including all add-ons and submissionTime - // set requestGeneration = 1 - snapshot.setRequestGeneration(1); - const record = _.assign( - {requestSynced: false, apiServerDeleted: false}, - snapshot.getAllUpdate(), - addOns.getUpdate(), + // if the old framework doesn't exist, create add-ons. + const addOns = new AddOns( + configSecretDef, + priorityClassDef, + dockerSecretDef, ); - record.submissionTime = new Date(submissionTime); - await databaseModel.Framework.create(record); - return [true, snapshot, addOns]; + return onCreateFrameworkRequest(snapshot, submissionTime, addOns); } else { - // update record in db + // If the old framework exists, we doesn't use the provided add-on def in body. + // Instead, we use the old record in database. const oldSnapshot = new Snapshot(oldFramework.snapshot); - // compare framework request (omit requestGeneration) - if ( - _.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true)) - ) { - // request is equal, no-op - return [false, snapshot, addOns]; - } else { - // request is different - // update request in db, mark requestSynced=false - snapshot.setRequestGeneration(oldSnapshot.getRequestGeneration() + 1); - await databaseModel.Framework.update( - _.assign({}, snapshot.getRequestUpdate(), { - requestSynced: false, - }), - { where: { name: frameworkName } }, - ); - return [true, snapshot, addOns]; - } + const addOns = new AddOns( + oldFramework.configSecretDef, + oldFramework.priorityClassDef, + oldFramework.dockerSecretDef, + ); + return onModifyFrameworkRequest(oldSnapshot, snapshot, addOns) } }, ); res.status(200).json({ message: 'ok' }); - // Poller has an interval to synchronize the framework request to API server. - // The interval will cause a delay on every job. - // So we introduce a short-cut here to synchronize the framework request. - // It is an async function call and doesn't affect the return of this function. - // Any error of it will be logged and ignored. - if (needSynchronize) { - silentSynchronizeRequest(snapshot, addOns); - } } catch (err) { return next(err); } @@ -184,6 +256,7 @@ async function receiveFrameworkRequest(req, res, next) { module.exports = { ping: ping, - receiveFrameworkRequest: receiveFrameworkRequest, - receiveWatchEvents: receiveWatchEvents, + putFrameworkRequest: putFrameworkRequest, + patchFrameworkRequest: patchFrameworkRequest, + postWatchEvents: postWatchEvents, }; diff --git a/src/database-controller/src/yarn.lock b/src/database-controller/src/yarn.lock index 655d8a9ba3..5e8b8f2a74 100644 --- a/src/database-controller/src/yarn.lock +++ b/src/database-controller/src/yarn.lock @@ -168,6 +168,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +array-filter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" + integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -219,6 +224,13 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" + integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== + dependencies: + array-filter "^1.0.0" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -529,6 +541,26 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +deep-equal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" + integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== + dependencies: + es-abstract "^1.17.5" + es-get-iterator "^1.1.0" + is-arguments "^1.0.4" + is-date-object "^1.0.2" + is-regex "^1.0.5" + isarray "^2.0.5" + object-is "^1.1.2" + object-keys "^1.1.1" + object.assign "^4.1.0" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.2" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.1" + which-typed-array "^1.1.2" + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -628,7 +660,7 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: version "1.17.6" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== @@ -645,6 +677,19 @@ es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" +es-get-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" + integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== + dependencies: + es-abstract "^1.17.4" + has-symbols "^1.0.1" + is-arguments "^1.0.4" + is-map "^2.0.1" + is-set "^2.0.1" + is-string "^1.0.5" + isarray "^2.0.5" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -1008,6 +1053,11 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -1344,22 +1394,37 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" + integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== + is-bluebird@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2" integrity sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI= +is-boolean-object@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" + integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== + is-callable@^1.1.4, is-callable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== @@ -1381,6 +1446,16 @@ is-glob@^4.0.0, is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" +is-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" + integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== + +is-number-object@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" + integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== + is-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" @@ -1391,6 +1466,13 @@ is-plain-obj@^1.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-regex@^1.0.5: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + dependencies: + has-symbols "^1.0.1" + is-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" @@ -1403,12 +1485,17 @@ is-retry-allowed@^1.1.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-set@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" + integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-string@^1.0.5: +is-string@^1.0.4, is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== @@ -1420,16 +1507,41 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.1" +is-typed-array@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" + integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== + dependencies: + available-typed-arrays "^1.0.0" + es-abstract "^1.17.4" + foreach "^2.0.5" + has-symbols "^1.0.1" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakset@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" + integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== + isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isemail@3.x.x: version "3.2.0" resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c" @@ -1492,6 +1604,13 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-merge-patch@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-merge-patch/-/json-merge-patch-1.0.0.tgz#8c8e78ce88535105332afb737b5920d6c321a2fa" + integrity sha512-pG2/EXnf8UvFuST3lNnhBaA0UROQPzrp/5hlK0FHCu7ha8RDKy9AhKlBjTXugY/xgWxeCThRmUHD8QxEKOw1Iw== + dependencies: + deep-equal "^2.0.3" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1788,6 +1907,14 @@ object-inspect@^1.7.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== +object-is@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" + integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -2232,6 +2359,14 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +regexp.prototype.flags@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -2442,6 +2577,14 @@ shimmer@^1.1.0: resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== +side-channel@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" + integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== + dependencies: + es-abstract "^1.17.0-next.1" + object-inspect "^1.7.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -2777,6 +2920,39 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +which-boxed-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" + integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== + dependencies: + is-bigint "^1.0.0" + is-boolean-object "^1.0.0" + is-number-object "^1.0.3" + is-string "^1.0.4" + is-symbol "^1.0.2" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" + integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== + dependencies: + available-typed-arrays "^1.0.2" + es-abstract "^1.17.5" + foreach "^2.0.5" + function-bind "^1.1.1" + has-symbols "^1.0.1" + is-typed-array "^1.1.3" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" diff --git a/src/rest-server/src/models/v2/job/k8s.js b/src/rest-server/src/models/v2/job/k8s.js index 360cec4c17..65ae0068ca 100644 --- a/src/rest-server/src/models/v2/job/k8s.js +++ b/src/rest-server/src/models/v2/job/k8s.js @@ -897,7 +897,7 @@ const put = async (frameworkName, config, rawConfig) => { try { response = await axios({ method: 'put', - url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest', + url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest/' + encodeName(frameworkName), data: { frameworkRequest: frameworkDescription, submissionTime: submissionTime, @@ -924,35 +924,17 @@ const put = async (frameworkName, config, rawConfig) => { const execute = async (frameworkName, executionType) => { let response; try { - const framework = await databaseModel.Framework.findOne({ - attributes: ['snapshot', 'submissionTime', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef'], - where: {name: encodeName(frameworkName)}, - }); - if (!framework) { - throw createError('Not Found', 'NoJobError', `Job ${frameworkName} is not found.`); + const patchData = { + spec: { + executionType: `${executionType.charAt(0)}${executionType.slice(1).toLowerCase()}` + } } - const snapshot = JSON.parse(framework.snapshot); - const frameworkRequest = _.pick(snapshot, [ - 'apiVersion', - 'kind', - 'metadata.name', - 'metadata.labels', - 'metadata.annotations', - 'spec', - ]); - frameworkRequest.spec.executionType = `${executionType.charAt(0)}${executionType.slice(1).toLowerCase()}`; response = await axios({ - method: 'put', - url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest', - data: { - frameworkRequest: frameworkRequest, - submissionTime: framework.submissionTime, - configSecretDef: framework.configSecretDef, - priorityClassDef: framework.priorityClassDef, - dockerSecretDef: framework.dockerSecretDef, - }, + method: 'PATCH', + url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest/' + encodeName(frameworkName), + data: patchData, headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/merge-patch+json', }, }); } catch (error) { From 2dce698440486e2aaaed391fbc60da9fd586ced5 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 10 Aug 2020 16:45:25 +0800 Subject: [PATCH 23/32] fluentd fix --- .../src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb b/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb index a67267460c..afadb1c841 100644 --- a/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb +++ b/src/fluentd/src/fluent-plugin-pgjson/lib/fluent/plugin/out_pgjson.rb @@ -7,6 +7,7 @@ require "pg" require "yajl" require "json" +require 'digest' module Fluent::Plugin class PgJsonOutput < Fluent::Plugin::Output @@ -152,11 +153,12 @@ def write(chunk) log.debug "log type: #{kind}" if kind == "Framework" thread[:conn].exec("COPY framework_history (\"#{@insertedAt_col}\", \"#{@updatedAt_col}\", \"#{@uid_col}\", \"#{@frameworkName_col}\", \"#{@attemptIndex_col}\", \"#{@historyType_col}\", \"#{@snapshot_col}\") FROM STDIN WITH DELIMITER E'\\x01'") - uid = (0...36).map { (65 + rand(26)).chr }.join frameworkName = record["objectSnapshot"]["metadata"]["name"] attemptIndex = record["objectSnapshot"]["status"]["attemptStatus"]["id"] historyType = "retry" snapshot = record_value(record["objectSnapshot"]) + # use frameworkName + attemptIndex + historyType to generate a uid + uid = Digest::MD5.hexdigest "#{frameworkName}+#{attemptIndex}+#{historyType}" thread[:conn].put_copy_data "#{time}\x01#{time}\x01#{uid}\x01#{frameworkName}\x01#{attemptIndex}\x01#{historyType}\x01#{snapshot}\n" elsif kind == "Pod" thread[:conn].exec("COPY pods (\"#{@insertedAt_col}\", \"#{@updatedAt_col}\", \"#{@uid_col}\", \"#{@frameworkName_col}\", \"#{@attemptIndex_col}\", \"#{@taskroleName_col}\", \"#{@taskroleIndex_col}\", \"#{@taskAttemptIndex_col}\", \"#{@snapshot_col}\") FROM STDIN WITH DELIMITER E'\\x01'") From e459a88978b17ea5ad6ff8151ee67f0862444d8b Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Mon, 10 Aug 2020 18:13:06 +0800 Subject: [PATCH 24/32] fix --- .../config/database-controller.yaml | 2 +- .../deploy/database-controller.yaml.template | 16 +- src/database-controller/src/common/config.js | 4 +- .../src/common/framework.js | 28 ++- src/database-controller/src/common/k8s.js | 9 +- src/database-controller/src/common/util.js | 2 +- .../src/initializer/index.js | 6 +- src/database-controller/src/poller/index.js | 6 +- .../src/write-merger/app.js | 10 +- .../src/write-merger/handler.js | 165 ++++++++++-------- src/database-controller/test-case.md | 10 +- src/rest-server/src/models/v2/job/k8s.js | 6 +- 12 files changed, 156 insertions(+), 108 deletions(-) diff --git a/src/database-controller/config/database-controller.yaml b/src/database-controller/config/database-controller.yaml index 37b1a1c7cb..fd980fbcc4 100644 --- a/src/database-controller/config/database-controller.yaml +++ b/src/database-controller/config/database-controller.yaml @@ -5,7 +5,7 @@ service_type: "common" # general settings log-level: info -recovery-mode: false +retain-mode: false k8s-connection-timeout-second: 120 write-merger-connection-timeout-second: 120 diff --git a/src/database-controller/deploy/database-controller.yaml.template b/src/database-controller/deploy/database-controller.yaml.template index b609f2f025..88b64c2a8b 100644 --- a/src/database-controller/deploy/database-controller.yaml.template +++ b/src/database-controller/deploy/database-controller.yaml.template @@ -31,8 +31,8 @@ spec: value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" - - name: RECOVERY_MODE_ENABLED -{% if cluster_cfg['database-controller']['recovery-mode'] %} + - name: RETAIN_MODE_ENABLED +{% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" @@ -60,8 +60,8 @@ spec: value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" - - name: RECOVERY_MODE_ENABLED -{% if cluster_cfg['database-controller']['recovery-mode'] %} + - name: RETAIN_MODE_ENABLED +{% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" @@ -102,8 +102,8 @@ spec: value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" - - name: RECOVERY_MODE_ENABLED -{% if cluster_cfg['database-controller']['recovery-mode'] %} + - name: RETAIN_MODE_ENABLED +{% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" @@ -130,8 +130,8 @@ spec: value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" - - name: RECOVERY_MODE_ENABLED -{% if cluster_cfg['database-controller']['recovery-mode'] %} + - name: RETAIN_MODE_ENABLED +{% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" diff --git a/src/database-controller/src/common/config.js b/src/database-controller/src/common/config.js index 1d432445c3..ccb1144c62 100644 --- a/src/database-controller/src/common/config.js +++ b/src/database-controller/src/common/config.js @@ -19,7 +19,7 @@ const configSchema = Joi.object() .optional(), customK8sCaFile: Joi.string().optional(), customK8sTokenFile: Joi.string().optional(), - recoveryModeEnabled: Joi.boolean().required(), + retainModeEnabled: Joi.boolean().required(), rbacEnabled: Joi.boolean().required(), }) .required(); @@ -35,7 +35,7 @@ const config = { customK8sApiServerURL: process.env.CUSTOM_K8S_API_SERVER_URL, customK8sCaFile: process.env.CUSTOM_K8S_CA_FILE, customK8sTokenFile: process.env.CUSTOM_K8S_TOKEN_FILE, - recoveryModeEnabled: process.env.RECOVERY_MODE_ENABLED === 'true', + retainModeEnabled: process.env.RETAIN_MODE_ENABLED === 'true', rbacEnabled: process.env.RBAC_IN_CLUSTER === 'true', }; diff --git a/src/database-controller/src/common/framework.js b/src/database-controller/src/common/framework.js index d259c12999..fa62899e61 100644 --- a/src/database-controller/src/common/framework.js +++ b/src/database-controller/src/common/framework.js @@ -58,7 +58,7 @@ const convertFrameworkState = (state, exitCode, retryDelaySec) => { } }; -const decompressField = (val) => { +const decompressField = val => { if (val == null) { return null; } else { @@ -138,8 +138,13 @@ class Snapshot { // This function decompress this field. // It is usually called before we write snapshot into database. const attemptStatus = this._snapshot.status.attemptStatus; - if ((!attemptStatus.taskRoleStatuses) && (attemptStatus.taskRoleStatusesCompressed)) { - attemptStatus.taskRoleStatuses = decompressField(attemptStatus.taskRoleStatusesCompressed); + if ( + !attemptStatus.taskRoleStatuses && + attemptStatus.taskRoleStatusesCompressed + ) { + attemptStatus.taskRoleStatuses = decompressField( + attemptStatus.taskRoleStatusesCompressed, + ); attemptStatus.taskRoleStatusesCompressed = null; } } @@ -172,7 +177,7 @@ class Snapshot { logPathInfix: this._snapshot.metadata.annotations.logPathInfix, }; if (withSnapshot) { - this.unzipTaskRoleStatuses() + this.unzipTaskRoleStatuses(); update.snapshot = JSON.stringify(this._snapshot); } return update; @@ -206,7 +211,7 @@ class Snapshot { ), }; if (withSnapshot) { - this.unzipTaskRoleStatuses() + this.unzipTaskRoleStatuses(); update.snapshot = JSON.stringify(this._snapshot); } return update; @@ -220,7 +225,7 @@ class Snapshot { this.getStatusUpdate(false), ); if (withSnapshot) { - this.unzipTaskRoleStatuses() + this.unzipTaskRoleStatuses(); update.snapshot = JSON.stringify(this._snapshot); } return update; @@ -461,7 +466,15 @@ function silentSynchronizeRequest(snapshot, addOns) { // any error will be ignored synchronizeRequest(snapshot, addOns).catch(logError); } catch (err) { - logError(err) + logError(err); + } +} + +function silentDeleteFramework(name) { + try { + k8s.deleteFramework(name).catch(logError); + } catch (err) { + logError(err); } } @@ -470,4 +483,5 @@ module.exports = { AddOns, synchronizeRequest, silentSynchronizeRequest, + silentDeleteFramework, }; diff --git a/src/database-controller/src/common/k8s.js b/src/database-controller/src/common/k8s.js index 5ec2125444..05f9f30d4c 100644 --- a/src/database-controller/src/common/k8s.js +++ b/src/database-controller/src/common/k8s.js @@ -79,8 +79,10 @@ async function createFramework(frameworkDescription, namespace = 'default') { async function patchFramework(name, data, namespace = 'default') { if (data.status) { - logger.warn('Modifying status field in framework is not allowed! Will delete it.') - delete data.status + logger.warn( + 'Modifying status field in framework is not allowed! Will delete it.', + ); + delete data.status; } const res = await customObjectsClient.patchNamespacedCustomObject( 'frameworkcontroller.microsoft.com', @@ -102,8 +104,7 @@ async function deleteFramework(name, namespace = 'default') { 'frameworks', name, ...Array(4), // skip some parameters - { headers: { 'propagationPolicy': 'Foreground' } }, - + { headers: { propagationPolicy: 'Foreground' } }, ); return res.response; } diff --git a/src/database-controller/src/common/util.js b/src/database-controller/src/common/util.js index 89761e1769..0355d14d1f 100644 --- a/src/database-controller/src/common/util.js +++ b/src/database-controller/src/common/util.js @@ -51,7 +51,7 @@ function alwaysRetryDecorator( } if (randomizeDelay) { // randomize between nextDelayMs * (0.8 ~ 1.2) - nextDelayMs = nextDelayMs * (Math.random() * 0.4 + 0.8) + nextDelayMs = nextDelayMs * (Math.random() * 0.4 + 0.8); } retryCount += 1; } diff --git a/src/database-controller/src/initializer/index.js b/src/database-controller/src/initializer/index.js index d4160e6274..119334c3e8 100644 --- a/src/database-controller/src/initializer/index.js +++ b/src/database-controller/src/initializer/index.js @@ -22,7 +22,7 @@ async function updateFromNoDatabaseVersion(databaseModel) { record.requestSynced = true; upsertPromises.push(databaseModel.Framework.upsert(record)); } - await Promise.all(upsertPromises) + await Promise.all(upsertPromises); } // This script should be idempotent. @@ -36,11 +36,11 @@ async function main() { await updateFromNoDatabaseVersion(databaseModel); } await databaseModel.setVersion(paiVersion, paiCommitVersion); - logger.info('Database has been successfully initialized.', function () { + logger.info('Database has been successfully initialized.', function() { process.exit(0); }); } catch (err) { - logger.error(err, function () { + logger.error(err, function() { process.exit(1); }); } diff --git a/src/database-controller/src/poller/index.js b/src/database-controller/src/poller/index.js index 96a9b18f50..42a68fe8c3 100644 --- a/src/database-controller/src/poller/index.js +++ b/src/database-controller/src/poller/index.js @@ -8,7 +8,11 @@ const { Sequelize } = require('sequelize'); const Op = Sequelize.Op; const DatabaseModel = require('openpaidbsdk'); const logger = require('@dbc/common/logger'); -const { Snapshot, AddOns, synchronizeRequest } = require('@dbc/common/framework'); +const { + Snapshot, + AddOns, + synchronizeRequest, +} = require('@dbc/common/framework'); const interval = require('interval-promise'); const config = require('@dbc/poller/config'); const fetch = require('node-fetch'); diff --git a/src/database-controller/src/write-merger/app.js b/src/database-controller/src/write-merger/app.js index 8d364c2893..403bd8cddf 100644 --- a/src/database-controller/src/write-merger/app.js +++ b/src/database-controller/src/write-merger/app.js @@ -16,15 +16,19 @@ const app = express(); app.use(cors()); app.use(compress()); app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ limit: config.bodyLimit, type: 'application/*'})); +app.use(bodyParser.json({ limit: config.bodyLimit, type: 'application/*' })); app.use(bodyParser.text({ type: 'text/*' })); app.use(morgan('dev', { stream: logger.stream })); const router = new express.Router(); router.route('/ping').get(handler.ping); -router.route('/frameworkRequest/:frameworkName').put(handler.putFrameworkRequest); -router.route('/frameworkRequest/:frameworkName').patch(handler.patchFrameworkRequest); +router + .route('/frameworkRequest/:frameworkName') + .put(handler.putFrameworkRequest); +router + .route('/frameworkRequest/:frameworkName') + .patch(handler.patchFrameworkRequest); router.route('/watchEvents/:eventType').post(handler.postWatchEvents); app.use('/api/v1', router); diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js index adf538437f..312d06896d 100644 --- a/src/database-controller/src/write-merger/handler.js +++ b/src/database-controller/src/write-merger/handler.js @@ -10,6 +10,7 @@ const { Snapshot, AddOns, silentSynchronizeRequest, + silentDeleteFramework, } = require('@dbc/common/framework'); const _ = require('lodash'); const lock = new AsyncLock({ maxPending: Number.MAX_SAFE_INTEGER }); @@ -43,7 +44,7 @@ async function ping(req, res, next) { async function postWatchEvents(req, res, next) { try { - const snapshot = new Snapshot(req.body); + let snapshot = new Snapshot(req.body); const frameworkName = snapshot.getName(); if (!frameworkName) { return next(createError(400, 'Cannot find framework name.')); @@ -55,21 +56,29 @@ async function postWatchEvents(req, res, next) { }); // database doesn't have the corresponding framework. if (!oldFramework) { - if (config.recoveryModeEnabled) { + if (config.retainModeEnabled) { // If database doesn't have the corresponding framework, - // and recovery mode is enabled + // and retain mode is enabled // tolerate the error and create framework in database. - const record = snapshot.getRecordForLegacyTransfer(); - record.requestSynced = true; - await databaseModel.Framework.create(record); + logger.warn( + `Framework ${frameworkName} appears in API server, and it is not in database. Tolerate it since retain mode is on.`, + ); } else { - throw createError(404, `Cannot find framework ${frameworkName}.`); + // If database doesn't have the corresponding framework, + // and retain mode is disabled, + // delete the framework silently + logger.warn( + `Framework ${frameworkName} appears in API server, and it is not in database. Delete it since retain mode is off.`, + ); + silentDeleteFramework(frameworkName); } } else { // Database has the corresponding framework. const oldSnapshot = new Snapshot(oldFramework.snapshot); const internalUpdate = {}; - if (oldSnapshot.getRequestGeneration() === snapshot.getRequestGeneration()) { + if ( + oldSnapshot.getRequestGeneration() === snapshot.getRequestGeneration() + ) { // if framework request is equal, mark requestSynced = true logger.info(`The request of framework ${frameworkName} is synced.`); internalUpdate.requestSynced = true; @@ -88,7 +97,7 @@ async function postWatchEvents(req, res, next) { // Event is DELETED and the state is not Completed. // This case could occur when someone deletes the framework in API server directly. // In such case, we mark requestSynced=false, and reset the snapshot using the framework request. - snapshot = new Snapshot(snapshot.getRequest(false)) + snapshot = new Snapshot(snapshot.getRequest(false)); internalUpdate.requestSynced = false; internalUpdate.apiServerDeleted = false; } @@ -111,7 +120,7 @@ async function onCreateFrameworkRequest(snapshot, submissionTime, addOns) { // set requestGeneration = 1 snapshot.setRequestGeneration(1); const record = _.assign( - {requestSynced: false, apiServerDeleted: false}, + { requestSynced: false, apiServerDeleted: false }, snapshot.getAllUpdate(), addOns.getUpdate(), ); @@ -127,9 +136,7 @@ async function onCreateFrameworkRequest(snapshot, submissionTime, addOns) { async function onModifyFrameworkRequest(oldSnapshot, snapshot, addOns) { // compare framework request (omit requestGeneration) - if ( - _.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true)) - ) { + if (_.isEqual(snapshot.getRequest(true), oldSnapshot.getRequest(true))) { // request is equal, no-op } else { // request is different @@ -162,38 +169,48 @@ async function patchFrameworkRequest(req, res, next) { if (!frameworkName) { return next(createError(400, 'Cannot find framework name.')); } - if (_.has(patchData, 'metadata.name') && patchData.metadata.name !== frameworkName) { - return next(createError(400, 'The framework names in query string doesn\'t match the name in body.')) + if ( + _.has(patchData, 'metadata.name') && + patchData.metadata.name !== frameworkName + ) { + return next( + createError( + 400, + "The framework names in query string doesn't match the name in body.", + ), + ); } - await lock.acquire( - frameworkName, - async () => { - const oldFramework = await databaseModel.Framework.findOne({ - attributes: ['snapshot', 'submissionTime', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef'], - where: { name: frameworkName }, - }); - if (!oldFramework) { - // if the old framework doesn't exist, throw a 404 error. - throw createError(404, `Cannot find framework ${frameworkName}.`); - } else { - // if the old framework exists - const oldSnapshot = new Snapshot(oldFramework.snapshot) - const snapshot = oldSnapshot.copy() - snapshot.applyRequestPatch(patchData); - const addOns = new AddOns( - oldFramework.configSecretDef, - oldFramework.priorityClassDef, - oldFramework.dockerSecretDef, - ); - return onModifyFrameworkRequest(oldSnapshot, snapshot, addOns) - } - }, - ); + await lock.acquire(frameworkName, async () => { + const oldFramework = await databaseModel.Framework.findOne({ + attributes: [ + 'snapshot', + 'submissionTime', + 'configSecretDef', + 'priorityClassDef', + 'dockerSecretDef', + ], + where: { name: frameworkName }, + }); + if (!oldFramework) { + // if the old framework doesn't exist, throw a 404 error. + throw createError(404, `Cannot find framework ${frameworkName}.`); + } else { + // if the old framework exists + const oldSnapshot = new Snapshot(oldFramework.snapshot); + const snapshot = oldSnapshot.copy(); + snapshot.applyRequestPatch(patchData); + const addOns = new AddOns( + oldFramework.configSecretDef, + oldFramework.priorityClassDef, + oldFramework.dockerSecretDef, + ); + return onModifyFrameworkRequest(oldSnapshot, snapshot, addOns); + } + }); res.status(200).json({ message: 'ok' }); } catch (err) { return next(err); } - } async function putFrameworkRequest(req, res, next) { @@ -217,37 +234,45 @@ async function putFrameworkRequest(req, res, next) { return next(createError(400, 'Cannot find framework name.')); } if (req.params.frameworkName !== frameworkName) { - return next(createError(400, 'The framework names in query string doesn\'t match the name in body.')) + return next( + createError( + 400, + "The framework names in query string doesn't match the name in body.", + ), + ); } - await lock.acquire( - frameworkName, - async () => { - const oldFramework = await databaseModel.Framework.findOne({ - attributes: ['snapshot', 'submissionTime', 'configSecretDef', 'priorityClassDef', 'dockerSecretDef'], - where: { name: frameworkName }, - }); - const snapshot = new Snapshot(frameworkRequest); - if (!oldFramework) { - // if the old framework doesn't exist, create add-ons. - const addOns = new AddOns( - configSecretDef, - priorityClassDef, - dockerSecretDef, - ); - return onCreateFrameworkRequest(snapshot, submissionTime, addOns); - } else { - // If the old framework exists, we doesn't use the provided add-on def in body. - // Instead, we use the old record in database. - const oldSnapshot = new Snapshot(oldFramework.snapshot); - const addOns = new AddOns( - oldFramework.configSecretDef, - oldFramework.priorityClassDef, - oldFramework.dockerSecretDef, - ); - return onModifyFrameworkRequest(oldSnapshot, snapshot, addOns) - } - }, - ); + await lock.acquire(frameworkName, async () => { + const oldFramework = await databaseModel.Framework.findOne({ + attributes: [ + 'snapshot', + 'submissionTime', + 'configSecretDef', + 'priorityClassDef', + 'dockerSecretDef', + ], + where: { name: frameworkName }, + }); + const snapshot = new Snapshot(frameworkRequest); + if (!oldFramework) { + // if the old framework doesn't exist, create add-ons. + const addOns = new AddOns( + configSecretDef, + priorityClassDef, + dockerSecretDef, + ); + return onCreateFrameworkRequest(snapshot, submissionTime, addOns); + } else { + // If the old framework exists, we doesn't use the provided add-on def in body. + // Instead, we use the old record in database. + const oldSnapshot = new Snapshot(oldFramework.snapshot); + const addOns = new AddOns( + oldFramework.configSecretDef, + oldFramework.priorityClassDef, + oldFramework.dockerSecretDef, + ); + return onModifyFrameworkRequest(oldSnapshot, snapshot, addOns); + } + }); res.status(200).json({ message: 'ok' }); } catch (err) { return next(err); diff --git a/src/database-controller/test-case.md b/src/database-controller/test-case.md index 37f2280c16..2d65e599db 100644 --- a/src/database-controller/test-case.md +++ b/src/database-controller/test-case.md @@ -99,19 +99,19 @@ Expect: The second request will be ignored. -#### (Write merger) If recovery mode, use the FR from API server +#### (Write merger) If retain mode is off, delete frameworks which are not submitted through db. -1. Environment: Turn on recovery mode +1. Environment: Turn on retain mode Case: Create a framework directly in API server; - Expect: The framework is inserted in database. + Expect: The framework is not deleted. -2. Environment: Turn off recovery mode +2. Environment: Turn off retain mode Case: Create a framework directly in API server; - Expect: The framework can't be inserted in database, write merger reports 404 error, and watcher keeps trying to synchroinze it. + Expect: The framework is deleted. #### (Write merger) If not equal, override and mark as !requestSynced diff --git a/src/rest-server/src/models/v2/job/k8s.js b/src/rest-server/src/models/v2/job/k8s.js index 65ae0068ca..4e053bbab7 100644 --- a/src/rest-server/src/models/v2/job/k8s.js +++ b/src/rest-server/src/models/v2/job/k8s.js @@ -926,9 +926,9 @@ const execute = async (frameworkName, executionType) => { try { const patchData = { spec: { - executionType: `${executionType.charAt(0)}${executionType.slice(1).toLowerCase()}` - } - } + executionType: `${executionType.charAt(0)}${executionType.slice(1).toLowerCase()}`, + }, + }; response = await axios({ method: 'PATCH', url: launcherConfig.writeMergerUrl + '/api/v1/frameworkRequest/' + encodeName(frameworkName), From a20d16fa06eaebd80f0eb02ba651d75f803c2451 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 11:20:28 +0800 Subject: [PATCH 25/32] fix --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3c895e7af1..44b8d844cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -91,10 +91,8 @@ matrix: env: NODE_ENV=test before_install: - cd src/rest-server - - cp -rf ../database-controller/sdk openpaidbsdk install: - yarn install - - rm -rf openpaidbsdk script: - npm test - npm run coveralls @@ -104,10 +102,8 @@ matrix: env: NODE_ENV=test before_install: - cd src/rest-server - - cp -rf ../database-controller/sdk openpaidbsdk install: - yarn install --ignore-engines - - rm -rf openpaidbsdk script: - npm test From 77352f4d4e0ef69b130edb646bb036f71195d137 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 11:43:42 +0800 Subject: [PATCH 26/32] fix --- .../config/database-controller.yaml | 10 ++++ .../deploy/database-controller.yaml.template | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/database-controller/config/database-controller.yaml b/src/database-controller/config/database-controller.yaml index fd980fbcc4..19d922f958 100644 --- a/src/database-controller/config/database-controller.yaml +++ b/src/database-controller/config/database-controller.yaml @@ -4,15 +4,25 @@ service_type: "common" # general settings +# Log level of all logs. You can choose from error, warn, info, http, verbose, debug, and silly. log-level: info +# Whether to enable retain mode. +# If someone submits a framework directly without accessing database, we can find the framework in write merger. +# For these frameworks, if retain mode is on, we will ignore them. +# If retain mode is off (it is the default setting), we will delete the frameworks to maintain ground-truth in database. retain-mode: false +# The global timeout for all calls to Kubernetes API server. k8s-connection-timeout-second: 120 +# The timeout for calls to write merger. write-merger-connection-timeout-second: 120 # write merger +# The serving port for write merger. write-merger-port: 9748 + # Max connection number to database in write merger. write-merger-max-db-connection: 50 # db poller +# Polling interval of database poller. Default value is 120. db-poller-interval-second: 120 db-poller-max-db-connection: 10 diff --git a/src/database-controller/deploy/database-controller.yaml.template b/src/database-controller/deploy/database-controller.yaml.template index 88b64c2a8b..b7d9cf6708 100644 --- a/src/database-controller/deploy/database-controller.yaml.template +++ b/src/database-controller/deploy/database-controller.yaml.template @@ -25,18 +25,28 @@ spec: image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} imagePullPolicy: Always env: + # Log level of all logs. You can choose from error, warn, info, http, verbose, debug, and silly. Default value is info. - name: LOG_LEVEL value: "{{ cluster_cfg['database-controller']['log-level'] }}" + # The global timeout for all calls to Kubernetes API server. Default value is 120. - name: K8S_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + # The timeout for calls to write merger. Default value is 120. - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + # Whether to enable retain mode. + # If someone submits a framework directly without accessing database, we can find the framework in write merger. + # For these frameworks, if retain mode is on, we will ignore them. + # If retain mode is off (it is the default setting), we will delete the frameworks to maintain ground-truth in database. - name: RETAIN_MODE_ENABLED {% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" {% endif %} + # If RBAC is set up in current environment. + # If RBAC_IN_CLUSTER=true, the API Server client can read all settings automatically in container. + # If RBAC_IN_CLUSTER=false, we should set CUSTOM_K8S_API_SERVER_URL. {% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} - name: RBAC_IN_CLUSTER value: "true" @@ -46,6 +56,7 @@ spec: - name: CUSTOM_K8S_API_SERVER_URL value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} + # The database client string. It follows the format "://:@:/" - name: DB_CONNECTION_STR value: "{{ cluster_cfg['postgresql']['connection-str'] }}" command: ["node", "initializer/index.js"] @@ -54,18 +65,28 @@ spec: image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} imagePullPolicy: Always env: + # Log level of all logs. You can choose from error, warn, info, http, verbose, debug, and silly. Default value is info. - name: LOG_LEVEL value: "{{ cluster_cfg['database-controller']['log-level'] }}" + # The global timeout for all calls to Kubernetes API server. Default value is 120. - name: K8S_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + # The timeout for calls to write merger. Default value is 120. - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + # Whether to enable retain mode. + # If someone submits a framework directly without accessing database, we can find the framework in write merger. + # For these frameworks, if retain mode is on, we will ignore them. + # If retain mode is off (it is the default setting), we will delete the frameworks to maintain ground-truth in database. - name: RETAIN_MODE_ENABLED {% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" {% endif %} + # If RBAC is set up in current environment. + # If RBAC_IN_CLUSTER=true, the API Server client can read all settings automatically in container. + # If RBAC_IN_CLUSTER=false, we should set CUSTOM_K8S_API_SERVER_URL. {% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} - name: RBAC_IN_CLUSTER value: "true" @@ -75,10 +96,13 @@ spec: - name: CUSTOM_K8S_API_SERVER_URL value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} + # The database client string. It follows the format "://:@:/" - name: DB_CONNECTION_STR value: "{{ cluster_cfg['postgresql']['connection-str'] }}" + # Max connection number to database in write merger. Default value is 50. - name: MAX_DB_CONNECTION value: "{{ cluster_cfg['database-controller']['write-merger-max-db-connection'] }}" + # The serving port for write merger. - name: PORT value: "{{ cluster_cfg['database-controller']['write-merger-port'] }}" readinessProbe: @@ -96,18 +120,28 @@ spec: image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} imagePullPolicy: Always env: + # Log level of all logs. You can choose from error, warn, info, http, verbose, debug, and silly. Default value is info. - name: LOG_LEVEL value: "{{ cluster_cfg['database-controller']['log-level'] }}" + # The global timeout for all calls to Kubernetes API server. Default value is 120. - name: K8S_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + # The timeout for calls to write merger. Default value is 120. - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + # Whether to enable retain mode. + # If someone submits a framework directly without accessing database, we can find the framework in write merger. + # For these frameworks, if retain mode is on, we will ignore them. + # If retain mode is off (it is the default setting), we will delete the frameworks to maintain ground-truth in database. - name: RETAIN_MODE_ENABLED {% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" {% endif %} + # If RBAC is set up in current environment. + # If RBAC_IN_CLUSTER=true, the API Server client can read all settings automatically in container. + # If RBAC_IN_CLUSTER=false, we should set CUSTOM_K8S_API_SERVER_URL. {% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} - name: RBAC_IN_CLUSTER value: "true" @@ -117,6 +151,7 @@ spec: - name: CUSTOM_K8S_API_SERVER_URL value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} + # The URL of write merger. - name: WRITE_MERGER_URL value: "{{ cluster_cfg['database-controller']['write-merger-url'] }}" command: ["node", "watcher/framework/index.js"] @@ -124,18 +159,28 @@ spec: image: {{ cluster_cfg["cluster"]["docker-registry"]["prefix"] }}database-controller:{{ cluster_cfg["cluster"]["docker-registry"]["tag"] }} imagePullPolicy: Always env: + # Log level of all logs. You can choose from error, warn, info, http, verbose, debug', and silly. Default value is info. - name: LOG_LEVEL value: "{{ cluster_cfg['database-controller']['log-level'] }}" + # The global timeout for all calls to Kubernetes API server. Default value is 120. - name: K8S_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['k8s-connection-timeout-second'] }}" + # The timeout for calls to write merger. Default value is 120. - name: WRITE_MERGER_CONNECTION_TIMEOUT_SECOND value: "{{ cluster_cfg['database-controller']['write-merger-connection-timeout-second'] }}" + # Whether to enable retain mode. + # If someone submits a framework directly without accessing database, we can find the framework in write merger. + # For these frameworks, if retain mode is on, we will ignore them. + # If retain mode is off (it is the default setting), we will delete the frameworks to maintain ground-truth in database. - name: RETAIN_MODE_ENABLED {% if cluster_cfg['database-controller']['retain-mode'] %} value: "true" {% else %} value: "false" {% endif %} + # If RBAC is set up in current environment. + # If RBAC_IN_CLUSTER=true, the API Server client can read all settings automatically in container. + # If RBAC_IN_CLUSTER=false, we should set CUSTOM_K8S_API_SERVER_URL. {% if cluster_cfg['cluster']['common']['k8s-rbac'] != 'false' %} - name: RBAC_IN_CLUSTER value: "true" @@ -145,12 +190,16 @@ spec: - name: CUSTOM_K8S_API_SERVER_URL value: {{ cluster_cfg['layout']['kubernetes']['api-servers-url'] }} {% endif %} + # The database client string. It follows the format "://:@:/" - name: DB_CONNECTION_STR value: "{{ cluster_cfg['postgresql']['connection-str'] }}" + # Max connection number to database in write merger. Default value is 10. - name: MAX_DB_CONNECTION value: "{{ cluster_cfg['database-controller']['db-poller-max-db-connection'] }}" + # Polling interval of database poller. Default value is 120. - name: INTERVAL_SECOND value: "{{ cluster_cfg['database-controller']['db-poller-interval-second'] }}" + # The URL of write merger. - name: WRITE_MERGER_URL value: "{{ cluster_cfg['database-controller']['write-merger-url'] }}" command: ["node", "poller/index.js"] From 735513174245e792cc8125612d9958e77bab83f9 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 13:47:16 +0800 Subject: [PATCH 27/32] fix --- src/database-controller/config/database-controller.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/database-controller/config/database-controller.yaml b/src/database-controller/config/database-controller.yaml index 19d922f958..2ad4e7074a 100644 --- a/src/database-controller/config/database-controller.yaml +++ b/src/database-controller/config/database-controller.yaml @@ -25,4 +25,5 @@ write-merger-max-db-connection: 50 # db poller # Polling interval of database poller. Default value is 120. db-poller-interval-second: 120 +# Max connection number to database in write merger. db-poller-max-db-connection: 10 From da4764087031cdea686d4f07e36b500f606fdbf5 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 16:45:01 +0800 Subject: [PATCH 28/32] fix --- docs/database-controller.md | 28 ++++++++++++++++++++++++++++ docs/images/dbc-structure.png | Bin 0 -> 151629 bytes 2 files changed, 28 insertions(+) create mode 100644 docs/database-controller.md create mode 100644 docs/images/dbc-structure.png diff --git a/docs/database-controller.md b/docs/database-controller.md new file mode 100644 index 0000000000..c869b3da55 --- /dev/null +++ b/docs/database-controller.md @@ -0,0 +1,28 @@ +# Database Controller + +
+ +
+ +Database Controller is designed to manage job status in database and API server. To be breif, we treat records in database as the ground truth, and synchronize them to the API server. + +Database Controller contains 3 main components: write merger, poller and watcher. Here is an example of job lifetime controlled by these 3 components: + +1. User submits framework request to rest-server. +2. Rest-server forwards the request to write merger. +3. Write merger saves it to database, mark `synced=false`, and return. +4. User is notified the framework request is successfully created. +5. Poller finds the `synced=false` request, and synchronize it to the API server. +6. Now watcher finds the framework is created in API server. So it sends the event to write merger. +7. Write merger receives the watched event, mark `synced=true`, and update job status according to the event. +8. The job finishes. Watcher sends this event to write merger. +9. Write merger receives the watched event, mark `completed=true`, and update job status according to the event. +10. Poller finds the `completed=true` request, delete it from API server. +11. Watcher sends the delete event to write merger. +12. Write merger receives the watched event, mark `deleted=true`. + +## Development + +**Environment:** Node.js 8.17.0, use `yarn install` to install all dependencies under `src/`. To set environmental variables, create a `.env` file under `src`. + +**Lint:** Run `npm run lint -- --fix` under `src/` or `sdk/`. diff --git a/docs/images/dbc-structure.png b/docs/images/dbc-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..b02c03d6d05ed04be9f0cbb608785bea3fd415b1 GIT binary patch literal 151629 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%0Dq-X{BEd@?;vz{f+NgI~3H^G~Q`8MJ?6X>;y7e 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 RWGdiKR|}$vRDbaF{{Wt4WDo!V literal 0 HcmV?d00001 From bcd1b3cbd14690d717e377e0e03c467345ada6ea Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 16:52:17 +0800 Subject: [PATCH 29/32] fix --- ...atabase-controller.md => database_controller.md} | 2 +- .../images/{dbc-structure.png => dbc_structure.png} | Bin src/database-controller/README.md | 7 +------ 3 files changed, 2 insertions(+), 7 deletions(-) rename docs/{database-controller.md => database_controller.md} (95%) rename docs/images/{dbc-structure.png => dbc_structure.png} (100%) diff --git a/docs/database-controller.md b/docs/database_controller.md similarity index 95% rename from docs/database-controller.md rename to docs/database_controller.md index c869b3da55..2b610b0ec2 100644 --- a/docs/database-controller.md +++ b/docs/database_controller.md @@ -4,7 +4,7 @@ -Database Controller is designed to manage job status in database and API server. To be breif, we treat records in database as the ground truth, and synchronize them to the API server. +Database Controller is designed to manage job status in database and API server. To be brief, we treat records in database as the ground truth, and synchronize them to the API server. Database Controller contains 3 main components: write merger, poller and watcher. Here is an example of job lifetime controlled by these 3 components: diff --git a/docs/images/dbc-structure.png b/docs/images/dbc_structure.png similarity index 100% rename from docs/images/dbc-structure.png rename to docs/images/dbc_structure.png diff --git a/src/database-controller/README.md b/src/database-controller/README.md index cad091e16b..1a9b866cfd 100644 --- a/src/database-controller/README.md +++ b/src/database-controller/README.md @@ -1,8 +1,3 @@ ## Database Controller -### Development - -**Environment:** Node.js 8.17.0, use `yarn install` to install all dependencies under `src/` or `sdk/`. To set environmental variables, create a `.env` file under `src`. - - -**Lint:** Run `npm run lintfix` under `src/` or `sdk/`. +See [here](../../docs/database_controller.md). From ae73bfe528f276b7bd39ec0b7ce54ff8f0b7c56a Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 16:53:39 +0800 Subject: [PATCH 30/32] fix --- src/database-controller/src/write-merger/handler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database-controller/src/write-merger/handler.js b/src/database-controller/src/write-merger/handler.js index 312d06896d..c6a46d095e 100644 --- a/src/database-controller/src/write-merger/handler.js +++ b/src/database-controller/src/write-merger/handler.js @@ -58,8 +58,8 @@ async function postWatchEvents(req, res, next) { if (!oldFramework) { if (config.retainModeEnabled) { // If database doesn't have the corresponding framework, - // and retain mode is enabled - // tolerate the error and create framework in database. + // and retain mode is enabled, + // retain the framework, i.e. do not delete it. logger.warn( `Framework ${frameworkName} appears in API server, and it is not in database. Tolerate it since retain mode is on.`, ); From e203fd4ad7dea8549f44693f508b0b0d9837b487 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Tue, 11 Aug 2020 17:03:57 +0800 Subject: [PATCH 31/32] fix --- docs/database_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database_controller.md b/docs/database_controller.md index 2b610b0ec2..a5cdaeeb6d 100644 --- a/docs/database_controller.md +++ b/docs/database_controller.md @@ -1,7 +1,7 @@ # Database Controller
- +
Database Controller is designed to manage job status in database and API server. To be brief, we treat records in database as the ground truth, and synchronize them to the API server. From 7c4878d99b8791d75bb49cd1dbc5dec3ddcb7d22 Mon Sep 17 00:00:00 2001 From: Zhiyuan He Date: Wed, 12 Aug 2020 13:50:13 +0800 Subject: [PATCH 32/32] fix --- src/database-controller/src/common/framework.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database-controller/src/common/framework.js b/src/database-controller/src/common/framework.js index fa62899e61..59459f8e4f 100644 --- a/src/database-controller/src/common/framework.js +++ b/src/database-controller/src/common/framework.js @@ -441,14 +441,14 @@ async function synchronizeRequest(snapshot, addOns) { // There may be multiple calls of synchronizeRequest. // Poller and write-merger uses this method. try { - await k8s.getFramework(snapshot.getName()); - // if framework exists + // Try to modify the framework const frameworkResponse = await synchronizeModify(snapshot); logger.info( `Request of framework ${snapshot.getName()} is successfully patched.`, ); return frameworkResponse; } catch (err) { + // If framework doesn't exist, create it. if (err.response && err.response.statusCode === 404) { const frameworkResponse = await synchronizeCreate(snapshot, addOns); logger.info(